diff --git a/application/Espo/Tools/EmailNotification/Processor.php b/application/Espo/Tools/EmailNotification/Processor.php index b8305a2cd5..bee79096a1 100644 --- a/application/Espo/Tools/EmailNotification/Processor.php +++ b/application/Espo/Tools/EmailNotification/Processor.php @@ -69,6 +69,8 @@ class Processor private const HOURS_THRESHOLD = 5; private const PROCESS_MAX_COUNT = 200; + private const TYPE_STATUS = 'Status'; + private ?Htmlizer $htmlizer = null; /** @var array */ @@ -210,8 +212,6 @@ class Processor protected function getNotificationQueryBuilderNote(): SelectBuilder { - $noteNotificationTypeList = $this->config->get('streamEmailNotificationsTypeList', []); - $builder = $this->entityManager ->getQueryBuilder() ->select() @@ -220,7 +220,7 @@ class Processor ->where([ 'type' => Notification::TYPE_NOTE, 'relatedType' => Note::ENTITY_TYPE, - 'note.type' => $noteNotificationTypeList, + 'note.type' => $this->getNoteNotificationTypeList(), ]); $entityList = $this->config->get('streamEmailNotificationsEntityList'); @@ -375,43 +375,34 @@ class Processor protected function processNotificationNote(Notification $notification): void { - if (!$notification->getRelated()) { + if ( + !$notification->getRelated() || + $notification->getRelated()->getEntityType() !== Note::ENTITY_TYPE + ) { return; } - if ($notification->getRelated()->getEntityType() !== Note::ENTITY_TYPE) { - return; - } + $noteId = $notification->getRelated()->getId(); - /** @var ?Note $note */ - $note = $this->entityManager->getEntityById(Note::ENTITY_TYPE, $notification->getRelated()->getId()); + $note = $this->entityManager->getRDBRepositoryByClass(Note::class)->getById($noteId); - if (!$note) { - return; - } - - $noteNotificationTypeList = $this->config->get('streamEmailNotificationsTypeList', []); - - if (!in_array($note->getType(), $noteNotificationTypeList)) { - return; - } - - if (!$notification->getUserId()) { + if ( + !$note || + !in_array($note->getType(), $this->getNoteNotificationTypeList()) || + !$notification->getUserId() + ) { return; } $userId = $notification->getUserId(); - /** @var ?User $user */ - $user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId); + $user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId); if (!$user) { return; } - $emailAddress = $user->getEmailAddress(); - - if (!$emailAddress) { + if (!$user->getEmailAddress()) { return; } @@ -433,7 +424,7 @@ class Processor return; } - if ($type === Note::TYPE_STATUS) { + if ($type === Note::TYPE_UPDATE && isset($note->getData()->statusValue)) { $this->processNotificationNoteStatus($note, $user); return; @@ -665,18 +656,16 @@ class Processor $noteData = $note->getData(); - if (!isset($noteData->value) || !isset($note->field)) { + $value = $noteData->statusValue ?? null; + $field = $noteData->statusField ?? null; + + if ($value === null || !$field || !is_string($field)) { return; } - $data['value'] = $noteData->value; - $data['field'] = $field = $noteData->field; - - if (!is_string($field)) { - return; - } - - $data['valueTranslated'] = $this->language->translateOption($data['value'], $data['field'], $parentType); + $data['value'] = $value; + $data['field'] = $field; + $data['valueTranslated'] = $this->language->translateOption($value, $field, $parentType); $data['fieldTranslated'] = $this->language->translateLabel($field, 'fields', $parentType); $data['fieldTranslatedLowerCase'] = Util::mbLowerCaseFirst($data['fieldTranslated']); @@ -688,23 +677,22 @@ class Processor $subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl); $subject = $this->getHtmlizer()->render( - $note, - $subjectTpl, - 'note-status-email-subject', - $data, - true + entity: $note, + template: $subjectTpl, + cacheId: 'note-status-email-subject', + additionalData: $data, + skipLinks: true, ); $body = $this->getHtmlizer()->render( - $note, - $bodyTpl, - 'note-status-email-body', - $data, - true + entity: $note, + template: $bodyTpl, + cacheId: 'note-status-email-body', + additionalData: $data, + skipLinks: true, ); - /** @var Email $email */ - $email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE); + $email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew(); $email ->setSubject($subject) @@ -725,10 +713,10 @@ class Processor $senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams; } + $sender = $this->emailSender->withParams($senderParams); + try { - $this->emailSender - ->withParams($senderParams) - ->send($email); + $sender->send($email); } catch (Exception $e) { $this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]); } @@ -883,4 +871,21 @@ class Processor /** @var PortalRepository */ return $this->entityManager->getRepository(Portal::ENTITY_TYPE); } + + /** + * @return string[] + */ + private function getNoteNotificationTypeList(): array + { + /** @var string[] $output */ + $output = $this->config->get('streamEmailNotificationsTypeList', []); + + if (in_array(self::TYPE_STATUS, $output)) { + $output[] = Note::TYPE_UPDATE; + + $output = array_values(array_filter($output, fn ($v) => $v !== self::TYPE_STATUS)); + } + + return $output; + } } diff --git a/application/Espo/Tools/Notification/RecordService.php b/application/Espo/Tools/Notification/RecordService.php index 25ee28e9a6..68b8b4f399 100644 --- a/application/Espo/Tools/Notification/RecordService.php +++ b/application/Espo/Tools/Notification/RecordService.php @@ -226,7 +226,6 @@ class RecordService Cond::in(Expr::column(Notification::ATTR_ACTION_ID), $actionIds), ) ) - ->where([Attribute::ID => $ids]) ->build(); $this->entityManager->getQueryExecutor()->execute($query); diff --git a/application/Espo/Tools/Stream/HookProcessor.php b/application/Espo/Tools/Stream/HookProcessor.php index 8f93fb1539..5bb014aa3b 100644 --- a/application/Espo/Tools/Stream/HookProcessor.php +++ b/application/Espo/Tools/Stream/HookProcessor.php @@ -61,8 +61,6 @@ class HookProcessor private $hasStreamCache = []; /** @var array */ private $isLinkObservableInStreamCache = []; - /** @var ?array */ - private $statusFields = null; private const FIELD_ASSIGNED_USERS = Field::ASSIGNED_USERS; @@ -453,16 +451,6 @@ class HookProcessor if (empty($options[SaveOption::SKIP_AUDITED])) { $this->service->handleAudited($entity, $options); - - $statusField = $this->getStatusField($entity->getEntityType()); - - if ( - $statusField && - $entity->get($statusField) && - $entity->isAttributeChanged($statusField) - ) { - $this->service->noteStatus($entity, $statusField, $options); - } } $multipleField = $this->metadata->get(['streamDefs', $entity->getEntityType(), 'followingUsersField']) ?? @@ -560,38 +548,6 @@ class HookProcessor } } - private function getStatusField(string $entityType): ?string - { - return $this->getStatusFields()[$entityType] ?? null; - } - - /** - * @return array - */ - private function getStatusFields(): array - { - if (is_null($this->statusFields)) { - $this->statusFields = []; - - /** @var array> $scopes */ - $scopes = $this->metadata->get('scopes', []); - - foreach ($scopes as $scope => $data) { - /** @var ?string $statusField */ - $statusField = $data['statusField'] ?? null; - - if (!$statusField) { - continue; - } - - $this->statusFields[$scope] = $statusField; - } - } - - /** @var array */ - return $this->statusFields; - } - /** * @param array $options */ diff --git a/application/Espo/Tools/Stream/NoteAccessControl.php b/application/Espo/Tools/Stream/NoteAccessControl.php index 79d8327f6f..52529a1c3f 100644 --- a/application/Espo/Tools/Stream/NoteAccessControl.php +++ b/application/Espo/Tools/Stream/NoteAccessControl.php @@ -31,17 +31,13 @@ namespace Espo\Tools\Stream; use Espo\Entities\Note; use Espo\Entities\User; - use Espo\Core\Utils\Acl\UserAclManagerProvider; class NoteAccessControl { - private UserAclManagerProvider $userAclManagerProvider; - - public function __construct(UserAclManagerProvider $userAclManagerProvider) - { - $this->userAclManagerProvider = $userAclManagerProvider; - } + public function __construct( + private UserAclManagerProvider $userAclManagerProvider, + ) {} public function apply(Note $note, User $user): void { @@ -69,9 +65,22 @@ class NoteAccessControl unset($data->attributes->became->$attribute); } + $statusField = $data->statusField ?? null; + + if ( + $statusField && + !$this->userAclManagerProvider->get($user) + ->checkField($user, $note->getParentType(), $statusField) + ) { + unset($data->statusField); + unset($data->statusValue); + unset($data->statusStyle); + } + $note->setData($data); } + // Legacy as of v9.2.0. if ($note->getType() === Note::TYPE_STATUS && $note->getParentType()) { $forbiddenFieldList = $this->userAclManagerProvider ->get($user) diff --git a/application/Espo/Tools/Stream/Service.php b/application/Espo/Tools/Stream/Service.php index d642418552..f8bfb77b95 100644 --- a/application/Espo/Tools/Stream/Service.php +++ b/application/Espo/Tools/Stream/Service.php @@ -878,6 +878,8 @@ class Service /** * @param array $options + * @deprecated As of v9.2.0. The update note carries the status information now. + * @todo Remove in v9.3.0. */ public function noteStatus(Entity $entity, string $field, array $options = []): void { @@ -1041,59 +1043,11 @@ class Service */ public function handleAudited(Entity $entity, array $options = []): void { - $auditedFields = $this->getAuditedFieldsData($entity); + [$updatedFieldList, $was, $became] = $this->getUpdates($entity); - $updatedFieldList = []; + $statusData = $this->getStatusUpdateDate($entity); - $was = []; - $became = []; - - foreach ($auditedFields as $field => $item) { - $updated = false; - - foreach ($item['actualList'] as $attribute) { - if ($entity->hasFetched($attribute) && $entity->isAttributeChanged($attribute)) { - $updated = true; - - break; - } - } - - if (!$updated) { - continue; - } - - $updatedFieldList[] = $field; - - foreach ($item['actualList'] as $attribute) { - $was[$attribute] = $entity->getFetched($attribute); - $became[$attribute] = $entity->get($attribute); - } - - foreach ($item['notActualList'] as $attribute) { - $was[$attribute] = $entity->getFetched($attribute); - $became[$attribute] = $entity->get($attribute); - } - - if ($item['fieldType'] === FieldType::LINK_PARENT) { - $wasParentType = $was[$field . 'Type']; - $wasParentId = $was[$field . 'Id']; - - if ( - $wasParentType && - $wasParentId && - $this->entityManager->hasRepository($wasParentType) - ) { - $wasParent = $this->entityManager->getEntityById($wasParentType, $wasParentId); - - if ($wasParent) { - $was[$field . 'Name'] = $wasParent->get(Field::NAME); - } - } - } - } - - if (count($updatedFieldList) === 0) { + if (count($updatedFieldList) === 0 && !$statusData) { return; } @@ -1108,6 +1062,7 @@ class Service 'was' => (object) $was, 'became' => (object) $became, ], + ...($statusData ?? []), ]); $noteOptions = []; @@ -1438,4 +1393,89 @@ class Service { return in_array($entityType, $this->config->get('streamEmailWithContentEntityTypeList', [])); } + + /** + * @return array{string[], array, array} + */ + private function getUpdates(Entity $entity): array + { + $auditedFields = $this->getAuditedFieldsData($entity); + + $updatedFieldList = []; + $was = []; + $became = []; + + foreach ($auditedFields as $field => $item) { + $updated = false; + + foreach ($item['actualList'] as $attribute) { + if ($entity->hasFetched($attribute) && $entity->isAttributeChanged($attribute)) { + $updated = true; + + break; + } + } + + if (!$updated) { + continue; + } + + $updatedFieldList[] = $field; + + foreach ($item['actualList'] as $attribute) { + $was[$attribute] = $entity->getFetched($attribute); + $became[$attribute] = $entity->get($attribute); + } + + foreach ($item['notActualList'] as $attribute) { + $was[$attribute] = $entity->getFetched($attribute); + $became[$attribute] = $entity->get($attribute); + } + + if ($item['fieldType'] === FieldType::LINK_PARENT) { + $wasParentType = $was[$field . 'Type']; + $wasParentId = $was[$field . 'Id']; + + if ( + $wasParentType && + $wasParentId && + $this->entityManager->hasRepository($wasParentType) + ) { + $wasParent = $this->entityManager->getEntityById($wasParentType, $wasParentId); + + if ($wasParent) { + $was[$field . 'Name'] = $wasParent->get(Field::NAME); + } + } + } + } + + return [$updatedFieldList, $was, $became]; + } + + /** + * @param Entity $entity + * @return ?array + */ + private function getStatusUpdateDate(Entity $entity): ?array + { + $statusField = $this->metadata->get("scopes.{$entity->getEntityType()}.statusField"); + $statusData = null; + + if ( + $statusField && + $entity->isAttributeChanged($statusField) && + $entity->get($statusField) + ) { + $style = $this->getStatusStyle($entity->getEntityType(), $statusField, $entity->get($statusField)); + + $statusData = [ + 'statusValue' => $entity->get($statusField), + 'statusField' => $statusField, + 'statusStyle' => $style, + ]; + } + + return $statusData; + } } diff --git a/client/res/templates/stream/notes/update.tpl b/client/res/templates/stream/notes/update.tpl index 84a1fdf913..cabce70eee 100644 --- a/client/res/templates/stream/notes/update.tpl +++ b/client/res/templates/stream/notes/update.tpl @@ -16,42 +16,45 @@ +{{#if statusText}} +
+ {{statusText}} +
+{{/if}} + {{#if fieldDataList.length}}
{{fieldsString}}
{{/if}} -
diff --git a/client/src/handlers/record/view-audit-log.js b/client/src/handlers/record/view-audit-log.js index fb906b9fe0..434b18eae5 100644 --- a/client/src/handlers/record/view-audit-log.js +++ b/client/src/handlers/record/view-audit-log.js @@ -36,11 +36,9 @@ class ViewAuditLogHandler { this.entityType = this.view.entityType; this.model = /** @type {module:model} */this.view.model; - this.hasAudited = ( - this.metadata.get(`scopes.${this.entityType}.stream`) && - this.metadata.get(`scopes.${this.entityType}.statusField`) - ) || - this.model.getFieldList().find(field => this.model.getFieldParam(field, 'audited')) !== undefined; + this.hasAudited = + this.metadata.get(`scopes.${this.entityType}.statusField`) || + this.model.getFieldList().find(field => this.model.getFieldParam(field, 'audited')) !== undefined; if (this.entityType === 'User' && !this.view.getUser().isAdmin()) { this.hasAudited = false; diff --git a/client/src/views/notification/fields/container.js b/client/src/views/notification/fields/container.js index 9492d03c2d..579562b515 100644 --- a/client/src/views/notification/fields/container.js +++ b/client/src/views/notification/fields/container.js @@ -200,13 +200,11 @@ class NotificationContainerFieldView extends BaseFieldView { this.isGroupExpanded = true; - if (this.model.attributes.read === false) { + /*if (this.model.attributes.read === false) { collection.models.forEach(model => { model.set('read', false); }); - } - - //console.log(this.getSelector()); + }*/ const view = new NotificationListRecordView({ collection: collection, diff --git a/client/src/views/stream/notes/status.js b/client/src/views/stream/notes/status.js index abc6ac1e38..48785575a6 100644 --- a/client/src/views/stream/notes/status.js +++ b/client/src/views/stream/notes/status.js @@ -28,6 +28,9 @@ import NoteStreamView from 'views/stream/note'; +/** + * Legacy as of v9.2.0. + */ class StatusNoteStreamView extends NoteStreamView { template = 'stream/notes/status' diff --git a/client/src/views/stream/notes/update.js b/client/src/views/stream/notes/update.js index 671952a05e..0b2383f91a 100644 --- a/client/src/views/stream/notes/update.js +++ b/client/src/views/stream/notes/update.js @@ -34,6 +34,9 @@ class UpdateNoteStreamView extends NoteStreamView { messageName = 'update' rowActionsView = 'views/stream/record/row-actions/update' + statusText + statusStyle + data() { const fieldsString = this.fieldDataList .map(it => it.label) @@ -45,6 +48,8 @@ class UpdateNoteStreamView extends NoteStreamView { parentType: this.model.get('parentType'), iconHtml: this.getIconHtml(), fieldsString: fieldsString, + statusText: this.statusText, + statusStyle: this.statusStyle, }; } @@ -61,13 +66,25 @@ class UpdateNoteStreamView extends NoteStreamView { this.createMessage(); + /** @type {Record} */ + const data = this.model.attributes.data; + + if (data.statusField) { + const statusField = this.statusField = data.statusField; + const statusValue = data.statusValue; + + this.statusStyle = data.statusStyle || 'default'; + this.statusText = this.getLanguage() + .translateOption(statusValue, statusField, this.model.attributes.parentType); + } + this.wait(true); this.getModelFactory().create(this.model.get('parentType'), model => { const modelWas = model; const modelBecame = model.clone(); - const data = this.model.get('data'); + data.attributes = data.attributes || {};