Merge branch 'f/notificaiton-grouping'
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
namespace Espo\Controllers;
|
namespace Espo\Controllers;
|
||||||
|
|
||||||
use Espo\Core\Name\Field;
|
use Espo\Core\Name\Field;
|
||||||
|
use Espo\Entities\Notification as NotificationEntity;
|
||||||
use Espo\Tools\Notification\RecordService as Service;
|
use Espo\Tools\Notification\RecordService as Service;
|
||||||
|
|
||||||
use Espo\Core\Api\Request;
|
use Espo\Core\Api\Request;
|
||||||
@@ -60,6 +61,7 @@ class Notification extends RecordBase
|
|||||||
$maxSize = $searchParamsAux->getMaxSize();
|
$maxSize = $searchParamsAux->getMaxSize();
|
||||||
|
|
||||||
$after = $request->getQueryParam('after');
|
$after = $request->getQueryParam('after');
|
||||||
|
$beforeNumber = $request->getQueryParam('beforeNumber');
|
||||||
|
|
||||||
$searchParams = SearchParams
|
$searchParams = SearchParams
|
||||||
::create()
|
::create()
|
||||||
@@ -76,19 +78,20 @@ class Notification extends RecordBase
|
|||||||
->setValue($after)
|
->setValue($after)
|
||||||
->build()
|
->build()
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$recordCollection = $this->getNotificationService()->get($this->user, $searchParams);
|
$recordCollection = $this->getNotificationService()->get($this->user, $searchParams, $beforeNumber);
|
||||||
|
|
||||||
return $recordCollection->toApiOutput();
|
return $recordCollection->toApiOutput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
public function getActionNotReadCount(): int
|
public function getActionNotReadCount(): int
|
||||||
{
|
{
|
||||||
$userId = $this->user->getId();
|
return $this->getNotificationService()->getNotReadCount($this->user);
|
||||||
|
|
||||||
return $this->getNotificationService()->getNotReadCount($userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function postActionMarkAllRead(Request $request): bool
|
public function postActionMarkAllRead(Request $request): bool
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* 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\Core\Upgrades\Migrations\V9_4;
|
||||||
|
|
||||||
|
use Espo\Core\Upgrades\Migration\Script;
|
||||||
|
use Espo\Entities\Preferences;
|
||||||
|
use Espo\Entities\User;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
|
||||||
|
class AfterUpgrade implements Script
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->updatePreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updatePreferences(): void
|
||||||
|
{
|
||||||
|
$users = $this->entityManager
|
||||||
|
->getRDBRepositoryByClass(User::class)
|
||||||
|
->sth()
|
||||||
|
->where([
|
||||||
|
User::ATTR_IS_ACTIVE => true,
|
||||||
|
User::ATTR_TYPE => [
|
||||||
|
User::TYPE_ADMIN,
|
||||||
|
User::TYPE_REGULAR,
|
||||||
|
User::TYPE_PORTAL,
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->find();
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$preferences = $this->entityManager->getRepositoryByClass(Preferences::class)->getById($user->getId());
|
||||||
|
|
||||||
|
if (!$preferences) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$preferences->set('notificationGrouping', true);
|
||||||
|
$this->entityManager->saveEntity($preferences);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,16 @@ class Note extends Entity
|
|||||||
public const TYPE_EMAIL_RECEIVED = 'EmailReceived';
|
public const TYPE_EMAIL_RECEIVED = 'EmailReceived';
|
||||||
public const TYPE_EMAIL_SENT = 'EmailSent';
|
public const TYPE_EMAIL_SENT = 'EmailSent';
|
||||||
|
|
||||||
|
public const string DATA_ATTR_ADDED_ASSIGNED_USERS = 'addedAssignedUsers';
|
||||||
|
public const string DATA_ATTR_REMOVED_ASSIGNED_USERS = 'removedAssignedUsers';
|
||||||
|
public const string DATA_ATTR_ASSIGNED_USERS = 'assignedUsers';
|
||||||
|
public const string DATA_ATTR_STATUS_VALUE = 'statusValue';
|
||||||
|
public const string DATA_ATTR_STATUS_FIELD = 'statusField';
|
||||||
|
public const string DATA_ATTR_ASSIGNED_USER_ID = 'assignedUserId';
|
||||||
|
public const string DATA_ATTR_ASSIGNED_USER_NAME = 'assignedUserName';
|
||||||
|
public const string DATA_ATTR_FIELDS = 'fields';
|
||||||
|
public const string DATA_ATTR_ATTRIBUTES = 'attributes';
|
||||||
|
|
||||||
private bool $aclIsProcessed = false;
|
private bool $aclIsProcessed = false;
|
||||||
|
|
||||||
public function isPost(): bool
|
public function isPost(): bool
|
||||||
|
|||||||
@@ -54,6 +54,22 @@ class Notification extends Entity
|
|||||||
public const ATTR_ACTION_ID = 'actionId';
|
public const ATTR_ACTION_ID = 'actionId';
|
||||||
public const ATTR_NUMBER = 'number';
|
public const ATTR_NUMBER = 'number';
|
||||||
|
|
||||||
|
public const string ATTR_RELATED_TYPE = 'relatedType';
|
||||||
|
public const string ATTR_RELATED_ID = 'relatedId';
|
||||||
|
public const string ATTR_RELATED_PARENT_TYPE = 'relatedParentType';
|
||||||
|
public const string ATTR_RELATED_PARENT_ID = 'relatedParentId';
|
||||||
|
|
||||||
|
public const string FIELD_DATA = 'data';
|
||||||
|
public const string FIELD_MESSAGE = 'message';
|
||||||
|
public const string FIELD_TYPE = 'type';
|
||||||
|
public const string FIELD_RELATED_PARENT = 'relatedParent';
|
||||||
|
public const string FIELD_IS_FEATURED = 'isFeatured';
|
||||||
|
|
||||||
|
public const string GROUP_TYPE_NOTE = Notification::TYPE_NOTE;
|
||||||
|
public const string GROUP_TYPE_EMAIL_RECEIVED = Notification::TYPE_EMAIL_RECEIVED;
|
||||||
|
|
||||||
|
public const string DATE_ATTR_NOTE_ID = 'noteId';
|
||||||
|
|
||||||
public function getType(): ?string
|
public function getType(): ?string
|
||||||
{
|
{
|
||||||
return $this->get('type');
|
return $this->get('type');
|
||||||
@@ -128,6 +144,12 @@ class Notification extends Entity
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRelatedParent(): ?LinkParent
|
||||||
|
{
|
||||||
|
/** @var ?LinkParent */
|
||||||
|
return $this->getValueObject('relatedParent');
|
||||||
|
}
|
||||||
|
|
||||||
public function setRelatedParent(LinkParent|Entity|null $relatedParent): self
|
public function setRelatedParent(LinkParent|Entity|null $relatedParent): self
|
||||||
{
|
{
|
||||||
if ($relatedParent instanceof LinkParent) {
|
if ($relatedParent instanceof LinkParent) {
|
||||||
@@ -185,8 +207,27 @@ class Notification extends Entity
|
|||||||
return $this->get('actionId');
|
return $this->get('actionId');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
public function setGroupedCount(?int $groupedCount): self
|
public function setGroupedCount(?int $groupedCount): self
|
||||||
{
|
{
|
||||||
return $this->set('groupedCount', $groupedCount);
|
return $this->set('groupedCount', $groupedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getGroupType(): ?string
|
||||||
|
{
|
||||||
|
return $this->get('groupType');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 9.4.0
|
||||||
|
*/
|
||||||
|
public function setIsFeatured(bool $isFeatured): self
|
||||||
|
{
|
||||||
|
return $this->set(self::FIELD_IS_FEATURED, $isFeatured);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,7 +338,8 @@
|
|||||||
"Reaction Removed": "Reaction Removed",
|
"Reaction Removed": "Reaction Removed",
|
||||||
"Reactions": "Reactions",
|
"Reactions": "Reactions",
|
||||||
"Lock": "Lock",
|
"Lock": "Lock",
|
||||||
"Unlock": "Unlock"
|
"Unlock": "Unlock",
|
||||||
|
"Assigned": "Assigned"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"checkLogsForDetails": "Check logs for details.",
|
"checkLogsForDetails": "Check logs for details.",
|
||||||
@@ -566,7 +567,12 @@
|
|||||||
"emailInbox": "{user} added email {entity} to your inbox",
|
"emailInbox": "{user} added email {entity} to your inbox",
|
||||||
"userPostReaction": "{user} reacted to your {post}",
|
"userPostReaction": "{user} reacted to your {post}",
|
||||||
"addedToCollaborators": "{user} added you as a collaborator to {entityType} {entity}",
|
"addedToCollaborators": "{user} added you as a collaborator to {entityType} {entity}",
|
||||||
"userPostInParentReaction": "{user} reacted to your {post} in {entityType} {entity}"
|
"userPostInParentReaction": "{user} reacted to your {post} in {entityType} {entity}",
|
||||||
|
"groupUpdatesMultiple": "{entityType} {entity} – {number} new activities",
|
||||||
|
"groupUpdatesOne": "{entityType} {entity} – new activity",
|
||||||
|
"groupUpdates": "{entityType} {entity}",
|
||||||
|
"groupEmailsReceivedNew": "Emails received – {number} new",
|
||||||
|
"groupEmailsReceived": "Emails received"
|
||||||
},
|
},
|
||||||
"streamMessages": {
|
"streamMessages": {
|
||||||
"post": "{user} posted on {entityType} {entity}",
|
"post": "{user} posted on {entityType} {entity}",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"assignmentEmailNotificationsIgnoreEntityTypeList": "Email assignment notifications",
|
"assignmentEmailNotificationsIgnoreEntityTypeList": "Email assignment notifications",
|
||||||
"reactionNotifications": "In-app notifications about reactions",
|
"reactionNotifications": "In-app notifications about reactions",
|
||||||
"reactionNotificationsNotFollowed": "Notifications about reactions for non-followed records",
|
"reactionNotificationsNotFollowed": "Notifications about reactions for non-followed records",
|
||||||
|
"notificationGrouping": "Group notifications",
|
||||||
"autoFollowEntityTypeList": "Global Auto-Follow",
|
"autoFollowEntityTypeList": "Global Auto-Follow",
|
||||||
"signature": "Email Signature",
|
"signature": "Email Signature",
|
||||||
"dashboardTabList": "Tab List",
|
"dashboardTabList": "Tab List",
|
||||||
|
|||||||
@@ -147,6 +147,10 @@
|
|||||||
[
|
[
|
||||||
{"name": "reactionNotifications"},
|
{"name": "reactionNotifications"},
|
||||||
{"name": "reactionNotificationsNotFollowed"}
|
{"name": "reactionNotificationsNotFollowed"}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{"name": "notificationGrouping"},
|
||||||
|
false
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
[
|
[
|
||||||
{"name": "reactionNotifications"},
|
{"name": "reactionNotifications"},
|
||||||
{"name": "reactionNotificationsNotFollowed"}
|
{"name": "reactionNotificationsNotFollowed"}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{"name": "notificationGrouping"},
|
||||||
|
false
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"controller": "controllers/notification",
|
"controller": "controllers/notification",
|
||||||
"acl": "acl/notification",
|
"acl": "acl/notification",
|
||||||
"aclPortal": "acl-portal/notification",
|
"aclPortal": "acl-portal/notification",
|
||||||
"collection": "collections/note",
|
"model": "models/notification",
|
||||||
|
"collection": "collections/notification",
|
||||||
"itemViews": {
|
"itemViews": {
|
||||||
"System": "views/notification/items/system",
|
"System": "views/notification/items/system",
|
||||||
"EmailInbox": "views/notification/items/email-inbox"
|
"EmailInbox": "views/notification/items/email-inbox"
|
||||||
|
|||||||
@@ -46,12 +46,33 @@
|
|||||||
"readOnly": true,
|
"readOnly": true,
|
||||||
"index": true
|
"index": true
|
||||||
},
|
},
|
||||||
|
"isFeatured": {
|
||||||
|
"type": "bool",
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
"groupedCount": {
|
"groupedCount": {
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"notStorable": true,
|
"notStorable": true,
|
||||||
"readOnly": true,
|
"readOnly": true,
|
||||||
"utility": true
|
"utility": true
|
||||||
},
|
},
|
||||||
|
"groupType": {
|
||||||
|
"type": "enum",
|
||||||
|
"notStorable": true,
|
||||||
|
"readOnly": true,
|
||||||
|
"utility": true,
|
||||||
|
"options": [
|
||||||
|
"",
|
||||||
|
"Note",
|
||||||
|
"EmailReceived"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"groupedUnreadCount": {
|
||||||
|
"type": "int",
|
||||||
|
"notStorable": true,
|
||||||
|
"readOnly": true,
|
||||||
|
"utility": true
|
||||||
|
},
|
||||||
"createdBy": {
|
"createdBy": {
|
||||||
"type": "link",
|
"type": "link",
|
||||||
"readOnly": true,
|
"readOnly": true,
|
||||||
|
|||||||
@@ -115,6 +115,10 @@
|
|||||||
"type": "bool",
|
"type": "bool",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
|
"notificationGrouping": {
|
||||||
|
"type": "bool",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
"autoFollowEntityTypeList": {
|
"autoFollowEntityTypeList": {
|
||||||
"type": "multiEnum",
|
"type": "multiEnum",
|
||||||
"view": "views/preferences/fields/auto-follow-entity-type-list",
|
"view": "views/preferences/fields/auto-follow-entity-type-list",
|
||||||
|
|||||||
@@ -355,6 +355,16 @@
|
|||||||
"method": "get",
|
"method": "get",
|
||||||
"actionClassName": "Espo\\Tools\\Notification\\Api\\GetGroup"
|
"actionClassName": "Espo\\Tools\\Notification\\Api\\GetGroup"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"route": "/Notification/group",
|
||||||
|
"method": "get",
|
||||||
|
"actionClassName": "Espo\\Tools\\Notification\\Api\\GetGroupAll"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/Notification/group/:id",
|
||||||
|
"method": "delete",
|
||||||
|
"actionClassName": "Espo\\Tools\\Notification\\Api\\DeleteGroup"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"route": "/EmailTemplate/:id/prepare",
|
"route": "/EmailTemplate/:id/prepare",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* 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\Api;
|
||||||
|
|
||||||
|
use Espo\Core\Api\Action;
|
||||||
|
use Espo\Core\Api\Request;
|
||||||
|
use Espo\Core\Api\Response;
|
||||||
|
use Espo\Core\Api\ResponseComposer;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Tools\Notification\GroupAllService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
class DeleteGroup implements Action
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private GroupAllService $service,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Request $request): Response
|
||||||
|
{
|
||||||
|
$id = $request->getRouteParam('id') ?? throw new BadRequest();
|
||||||
|
|
||||||
|
$this->service->remove($id);
|
||||||
|
|
||||||
|
return ResponseComposer::json(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* 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\Api;
|
||||||
|
|
||||||
|
use Espo\Core\Api\Action;
|
||||||
|
use Espo\Core\Api\Request;
|
||||||
|
use Espo\Core\Api\Response;
|
||||||
|
use Espo\Core\Api\ResponseComposer;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Core\Record\SearchParamsFetcher;
|
||||||
|
use Espo\Core\Select\Where\Item as WhereItem;
|
||||||
|
use Espo\Entities\Notification;
|
||||||
|
use Espo\Tools\Notification\GroupAllService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
class GetGroupAll implements Action
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private GroupAllService $service,
|
||||||
|
private SearchParamsFetcher $searchParamsFetcher,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Request $request): Response
|
||||||
|
{
|
||||||
|
$type = $request->getQueryParam('type') ?? throw new BadRequest("No `type`.");
|
||||||
|
$id = $request->getQueryParam('id') ?? throw new BadRequest("No `id`.");
|
||||||
|
|
||||||
|
$searchParams = $this->searchParamsFetcher->fetch($request);
|
||||||
|
|
||||||
|
$beforeNumber = $request->getQueryParam('beforeNumber');
|
||||||
|
|
||||||
|
if ($beforeNumber) {
|
||||||
|
$searchParams = $searchParams
|
||||||
|
->withWhereAdded(
|
||||||
|
WhereItem
|
||||||
|
::createBuilder()
|
||||||
|
->setAttribute(Notification::ATTR_NUMBER)
|
||||||
|
->setType(WhereItem\Type::LESS_THAN)
|
||||||
|
->setValue($beforeNumber)
|
||||||
|
->build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$collection = $this->service->get($type, $id, $searchParams);
|
||||||
|
|
||||||
|
return ResponseComposer::json(
|
||||||
|
$collection->toApiOutput()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
use Espo\Core\Acl;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
|
use Espo\Core\Record\Collection as RecordCollection;
|
||||||
|
use Espo\Core\Select\SearchParams;
|
||||||
|
use Espo\Core\Select\SelectBuilderFactory;
|
||||||
|
use Espo\Entities\Notification;
|
||||||
|
use Espo\Entities\User;
|
||||||
|
use Espo\ORM\Collection;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
use Espo\ORM\Name\Attribute;
|
||||||
|
use Espo\ORM\Query\DeleteBuilder;
|
||||||
|
use Espo\ORM\Query\Part\Condition as Cond;
|
||||||
|
use Espo\ORM\Query\Part\Expression as Expr;
|
||||||
|
use Espo\ORM\Query\Select;
|
||||||
|
use Espo\ORM\Query\SelectBuilder;
|
||||||
|
|
||||||
|
class GroupAllService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager,
|
||||||
|
private Acl $acl,
|
||||||
|
private User $user,
|
||||||
|
private RecordService $recordService,
|
||||||
|
private SelectBuilderFactory $selectBuilderFactory,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return RecordCollection<Notification>
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
public function get(string $type, string $groupId, SearchParams $searchParams): RecordCollection
|
||||||
|
{
|
||||||
|
$collection = null;
|
||||||
|
|
||||||
|
if ($type == Notification::GROUP_TYPE_NOTE) {
|
||||||
|
$collection = $this->getNote($groupId, $searchParams);
|
||||||
|
} else if ($type == Notification::GROUP_TYPE_EMAIL_RECEIVED) {
|
||||||
|
$collection = $this->getEmailReceived($searchParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$collection) {
|
||||||
|
throw new BadRequest("Bad group.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->markAsRead($collection);
|
||||||
|
|
||||||
|
return $collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return RecordCollection<Notification>
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function getNote(string $groupId, SearchParams $searchParams): RecordCollection
|
||||||
|
{
|
||||||
|
if (substr_count($groupId, '_') < 2) {
|
||||||
|
throw new BadRequest("Bad ID.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[, $entityType, $id] = explode('_', $groupId, 3);
|
||||||
|
|
||||||
|
if (!$this->acl->checkScope($entityType)) {
|
||||||
|
/** @var Collection<Notification> $collection */
|
||||||
|
$collection = $this->entityManager->getCollectionFactory()->create(Notification::ENTITY_TYPE);
|
||||||
|
|
||||||
|
return RecordCollection::create($collection, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = $this->prepareBuilder($searchParams);
|
||||||
|
|
||||||
|
$query = $builder
|
||||||
|
->where([
|
||||||
|
Notification::FIELD_TYPE => Notification::TYPE_NOTE,
|
||||||
|
Notification::ATTR_RELATED_PARENT_TYPE => $entityType,
|
||||||
|
Notification::ATTR_RELATED_PARENT_ID => $id,
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
[$collection, $total] = $this->runQuery($query);
|
||||||
|
|
||||||
|
$collection = $this->recordService->prepareCollection($collection, $this->user);
|
||||||
|
|
||||||
|
return RecordCollection::create($collection, $total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return RecordCollection<Notification>
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function getEmailReceived(SearchParams $searchParams): RecordCollection
|
||||||
|
{
|
||||||
|
$builder = $this->prepareBuilder($searchParams);
|
||||||
|
|
||||||
|
$query = $builder
|
||||||
|
->where([
|
||||||
|
Notification::FIELD_TYPE => Notification::TYPE_EMAIL_RECEIVED,
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
[$collection, $total] = $this->runQuery($query);
|
||||||
|
|
||||||
|
$collection = $this->recordService->prepareCollection($collection, $this->user);
|
||||||
|
|
||||||
|
return RecordCollection::create($collection, $total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: Collection<Notification>, 1: int}
|
||||||
|
*/
|
||||||
|
private function runQuery(Select $query): array
|
||||||
|
{
|
||||||
|
$collection = $this->entityManager
|
||||||
|
->getRDBRepositoryByClass(Notification::class)
|
||||||
|
->clone($query)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
$total = $this->entityManager
|
||||||
|
->getRDBRepositoryByClass(Notification::class)
|
||||||
|
->clone($query)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return [$collection, $total];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $ids
|
||||||
|
*/
|
||||||
|
private function markAsReadByIds(array $ids): void
|
||||||
|
{
|
||||||
|
if ($ids === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->entityManager
|
||||||
|
->getQueryBuilder()
|
||||||
|
->update()
|
||||||
|
->in(Notification::ENTITY_TYPE)
|
||||||
|
->set([Notification::ATTR_READ => true])
|
||||||
|
->where([Notification::ATTR_USER_ID => $this->user->getId()])
|
||||||
|
->where(
|
||||||
|
Cond::in(Expr::column(Attribute::ID), $ids)
|
||||||
|
)
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$this->entityManager->getQueryExecutor()->execute($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param RecordCollection<Notification> $collection
|
||||||
|
*/
|
||||||
|
private function markAsRead(RecordCollection $collection): void
|
||||||
|
{
|
||||||
|
$ids = [];
|
||||||
|
|
||||||
|
foreach ($collection->getCollection() as $entity) {
|
||||||
|
$ids[] = $entity->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->markAsReadByIds($ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return SelectBuilder
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function prepareBuilder(SearchParams $searchParams): SelectBuilder
|
||||||
|
{
|
||||||
|
return $this->selectBuilderFactory
|
||||||
|
->create()
|
||||||
|
->from(Notification::ENTITY_TYPE)
|
||||||
|
->withSearchParams($searchParams)
|
||||||
|
->withComplexExpressionsForbidden()
|
||||||
|
->buildQueryBuilder()
|
||||||
|
->where([
|
||||||
|
Notification::ATTR_USER_ID => $this->user->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequest
|
||||||
|
*/
|
||||||
|
public function remove(string $groupId): void
|
||||||
|
{
|
||||||
|
if (substr_count($groupId, '_') < 1) {
|
||||||
|
throw new BadRequest("Bad ID.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[$type,] = explode('_', $groupId, 2);
|
||||||
|
|
||||||
|
if ($type == Notification::GROUP_TYPE_NOTE) {
|
||||||
|
$this->removeNote($groupId);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type == Notification::GROUP_TYPE_EMAIL_RECEIVED) {
|
||||||
|
$this->removeEmailReceived();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequest("Bad group type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequest
|
||||||
|
*/
|
||||||
|
private function removeNote(string $groupId): void
|
||||||
|
{
|
||||||
|
if (substr_count($groupId, '_') < 2) {
|
||||||
|
throw new BadRequest("Bad ID.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[, $entityType, $id] = explode('_', $groupId, 3);
|
||||||
|
|
||||||
|
$query = DeleteBuilder::create()
|
||||||
|
->from(Notification::ENTITY_TYPE)
|
||||||
|
->where([
|
||||||
|
Notification::ATTR_USER_ID => $this->user->getId(),
|
||||||
|
])
|
||||||
|
->where([
|
||||||
|
Notification::FIELD_TYPE => Notification::TYPE_NOTE,
|
||||||
|
Notification::ATTR_RELATED_PARENT_TYPE => $entityType,
|
||||||
|
Notification::ATTR_RELATED_PARENT_ID => $id,
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$this->entityManager->getQueryExecutor()->execute($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeEmailReceived(): void
|
||||||
|
{
|
||||||
|
$query = DeleteBuilder::create()
|
||||||
|
->from(Notification::ENTITY_TYPE)
|
||||||
|
->where([
|
||||||
|
Notification::ATTR_USER_ID => $this->user->getId(),
|
||||||
|
])
|
||||||
|
->where([
|
||||||
|
Notification::FIELD_TYPE => Notification::TYPE_EMAIL_RECEIVED,
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$this->entityManager->getQueryExecutor()->execute($query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,12 +48,35 @@ use Espo\ORM\EntityManager;
|
|||||||
use Espo\ORM\Name\Attribute;
|
use Espo\ORM\Name\Attribute;
|
||||||
use Espo\ORM\Query\Part\Condition as Cond;
|
use Espo\ORM\Query\Part\Condition as Cond;
|
||||||
use Espo\ORM\Query\Part\Expression as Expr;
|
use Espo\ORM\Query\Part\Expression as Expr;
|
||||||
|
use Espo\ORM\Query\Part\Order;
|
||||||
|
use Espo\ORM\Query\Part\Selection;
|
||||||
use Espo\ORM\Query\Part\WhereItem;
|
use Espo\ORM\Query\Part\WhereItem;
|
||||||
use Espo\ORM\Query\SelectBuilder;
|
use Espo\ORM\Query\SelectBuilder;
|
||||||
|
use Espo\ORM\Query\Union;
|
||||||
|
use Espo\ORM\Query\UnionBuilder;
|
||||||
|
use Espo\ORM\Repository\RDBSelectBuilder;
|
||||||
use Espo\Tools\Stream\NoteAccessControl;
|
use Espo\Tools\Stream\NoteAccessControl;
|
||||||
|
use Espo\Tools\User\PreferencesProvider;
|
||||||
|
use PDO;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
class RecordService
|
class RecordService
|
||||||
{
|
{
|
||||||
|
private const string COLUMN_GROUP_TYPE = 'groupType';
|
||||||
|
private const string COLUMN_GROUP_UNREAD_COUNT = 'groupedUnreadCount';
|
||||||
|
|
||||||
|
/** @var string[] */
|
||||||
|
private array $noGroupAttributes = [
|
||||||
|
Attribute::DELETED,
|
||||||
|
Field::CREATED_BY . 'Id',
|
||||||
|
Notification::ATTR_ACTION_ID,
|
||||||
|
Notification::FIELD_DATA,
|
||||||
|
Notification::FIELD_MESSAGE,
|
||||||
|
Notification::FIELD_TYPE,
|
||||||
|
Notification::ATTR_RELATED_ID,
|
||||||
|
Notification::ATTR_RELATED_TYPE,
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EntityManager $entityManager,
|
private EntityManager $entityManager,
|
||||||
private Acl $acl,
|
private Acl $acl,
|
||||||
@@ -61,6 +84,7 @@ class RecordService
|
|||||||
private NoteAccessControl $noteAccessControl,
|
private NoteAccessControl $noteAccessControl,
|
||||||
private SelectBuilderFactory $selectBuilderFactory,
|
private SelectBuilderFactory $selectBuilderFactory,
|
||||||
private Config $config,
|
private Config $config,
|
||||||
|
private PreferencesProvider $preferencesProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,20 +94,14 @@ class RecordService
|
|||||||
* @throws Error
|
* @throws Error
|
||||||
* @throws BadRequest
|
* @throws BadRequest
|
||||||
* @throws Forbidden
|
* @throws Forbidden
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
public function get(User $user, SearchParams $searchParams): RecordCollection
|
public function get(User $user, SearchParams $searchParams, ?string $beforeNumber = null): RecordCollection
|
||||||
{
|
{
|
||||||
$queryBuilder = $this->selectBuilderFactory
|
$queryBuilder = $this->isGroupingEnabled($user) ?
|
||||||
->create()
|
$this->prepareGroupingQueryBuilder($user, $searchParams, beforeNumber: $beforeNumber) :
|
||||||
->from(Notification::ENTITY_TYPE)
|
$this->prepareQueryBuilder($user, $searchParams, beforeNumber: $beforeNumber);
|
||||||
->withSearchParams($searchParams)
|
|
||||||
->buildQueryBuilder()
|
|
||||||
->where([Notification::ATTR_USER_ID => $user->getId()])
|
|
||||||
->order(Notification::ATTR_NUMBER, SearchParams::ORDER_DESC);
|
|
||||||
|
|
||||||
if ($this->isGroupingEnabled()) {
|
|
||||||
$queryBuilder->where($this->getActionIdWhere($user->getId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
$offset = $searchParams->getOffset();
|
$offset = $searchParams->getOffset();
|
||||||
$limit = $searchParams->getMaxSize();
|
$limit = $searchParams->getMaxSize();
|
||||||
@@ -92,23 +110,16 @@ class RecordService
|
|||||||
$queryBuilder->limit($offset, $limit + 1);
|
$queryBuilder->limit($offset, $limit + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ignoreScopeList = $this->getIgnoreScopeList();
|
|
||||||
|
|
||||||
if ($ignoreScopeList !== []) {
|
|
||||||
$queryBuilder->where([
|
|
||||||
'OR' => [
|
|
||||||
'relatedParentType' => null,
|
|
||||||
'relatedParentType!=' => $ignoreScopeList,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = $queryBuilder->build();
|
$query = $queryBuilder->build();
|
||||||
|
|
||||||
|
if ($query instanceof Union) {
|
||||||
|
$collection = $this->fetchAndPrepareCollectionFromUnion($query);
|
||||||
|
} else {
|
||||||
$collection = $this->entityManager
|
$collection = $this->entityManager
|
||||||
->getRDBRepositoryByClass(Notification::class)
|
->getRDBRepositoryByClass(Notification::class)
|
||||||
->clone($query)
|
->clone($query)
|
||||||
->find();
|
->find();
|
||||||
|
}
|
||||||
|
|
||||||
if (!$collection instanceof EntityCollection) {
|
if (!$collection instanceof EntityCollection) {
|
||||||
throw new Error("Collection is not instance of EntityCollection.");
|
throw new Error("Collection is not instance of EntityCollection.");
|
||||||
@@ -116,7 +127,7 @@ class RecordService
|
|||||||
|
|
||||||
$collection = $this->prepareCollection($collection, $user);
|
$collection = $this->prepareCollection($collection, $user);
|
||||||
|
|
||||||
$groupedCountMap = $this->getGroupedCountMap($collection, $user->getId());
|
$groupedCountMap = $this->getActionGroupedCountMap($collection, $user->getId());
|
||||||
|
|
||||||
$ids = [];
|
$ids = [];
|
||||||
$actionIds = [];
|
$actionIds = [];
|
||||||
@@ -126,11 +137,19 @@ class RecordService
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$entity->getGroupType()) {
|
||||||
$ids[] = $entity->getId();
|
$ids[] = $entity->getId();
|
||||||
|
}
|
||||||
|
|
||||||
$groupedCount = null;
|
$groupedCount = null;
|
||||||
|
|
||||||
if ($entity->getActionId() && $this->isGroupingEnabled()) {
|
if ($this->isGroupingEnabled($user) && $entity->getGroupType()) {
|
||||||
|
$groupedCount = -1;
|
||||||
|
|
||||||
|
$entity->loadParentNameField(Notification::FIELD_RELATED_PARENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entity->getActionId() && $this->isActionGroupingEnabled()) {
|
||||||
$actionIds[] = $entity->getActionId();
|
$actionIds[] = $entity->getActionId();
|
||||||
|
|
||||||
$groupedCount = $groupedCountMap[$entity->getActionId()] ?? 0;
|
$groupedCount = $groupedCountMap[$entity->getActionId()] ?? 0;
|
||||||
@@ -252,33 +271,35 @@ class RecordService
|
|||||||
$entity->set('noteData', $note->getValueMap());
|
$entity->set('noteData', $note->getValueMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNotReadCount(string $userId): int
|
/**
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
public function getNotReadCount(User $user): int
|
||||||
{
|
{
|
||||||
$whereClause = [
|
$searchParams = SearchParams::create();
|
||||||
Notification::ATTR_USER_ID => $userId,
|
|
||||||
Notification::ATTR_READ => false,
|
|
||||||
];
|
|
||||||
|
|
||||||
$ignoreScopeList = $this->getIgnoreScopeList();
|
$queryBuilder = $this->isGroupingEnabled($user) ?
|
||||||
|
$this->prepareGroupingQueryBuilder($user, $searchParams, true) :
|
||||||
|
$this->prepareQueryBuilder($user, $searchParams, true);
|
||||||
|
|
||||||
if (count($ignoreScopeList)) {
|
$countQuery = $this->entityManager->getQueryBuilder()
|
||||||
$whereClause[] = [
|
->select()
|
||||||
'OR' => [
|
->fromQuery($queryBuilder->build(), 'q')
|
||||||
'relatedParentType' => null,
|
->select('COUNT:(q.id)', 'c')
|
||||||
'relatedParentType!=' => $ignoreScopeList,
|
->build();
|
||||||
]
|
|
||||||
];
|
$sth = $this->entityManager->getQueryExecutor()->execute($countQuery);
|
||||||
|
|
||||||
|
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$count = $row['c'] ?? null;
|
||||||
|
|
||||||
|
if (!is_int($count)) {
|
||||||
|
throw new UnexpectedValueException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder = $this->entityManager
|
return $count;
|
||||||
->getRDBRepositoryByClass(Notification::class)
|
|
||||||
->where($whereClause);
|
|
||||||
|
|
||||||
if ($this->isGroupingEnabled()) {
|
|
||||||
$builder->where($this->getActionIdWhere($userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $builder->count();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function markAllRead(string $userId): bool
|
public function markAllRead(string $userId): bool
|
||||||
@@ -422,9 +443,9 @@ class RecordService
|
|||||||
* @param EntityCollection<Notification> $collection
|
* @param EntityCollection<Notification> $collection
|
||||||
* @return array<string, int>
|
* @return array<string, int>
|
||||||
*/
|
*/
|
||||||
private function getGroupedCountMap(EntityCollection $collection, string $userId): array
|
private function getActionGroupedCountMap(EntityCollection $collection, string $userId): array
|
||||||
{
|
{
|
||||||
if (!$this->isGroupingEnabled()) {
|
if (!$this->isActionGroupingEnabled()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +485,7 @@ class RecordService
|
|||||||
return $groupedCountMap;
|
return $groupedCountMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isGroupingEnabled(): bool
|
private function isActionGroupingEnabled(): bool
|
||||||
{
|
{
|
||||||
// @todo Param in preferences?
|
// @todo Param in preferences?
|
||||||
return (bool) ($this->config->get('notificationGrouping') ?? true);
|
return (bool) ($this->config->get('notificationGrouping') ?? true);
|
||||||
@@ -483,4 +504,394 @@ class RecordService
|
|||||||
$entity->set('createdByName', $createdByName);
|
$entity->set('createdByName', $createdByName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isGroupingEnabled(User $user): bool
|
||||||
|
{
|
||||||
|
return $this->preferencesProvider->tryGet($user->getId())?->get('notificationGrouping') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function prepareGroupingQueryBuilder(
|
||||||
|
User $user,
|
||||||
|
SearchParams $searchParams,
|
||||||
|
bool $notRead = false,
|
||||||
|
?string $beforeNumber = null,
|
||||||
|
): UnionBuilder {
|
||||||
|
|
||||||
|
$noteGroupBuilder = $this->prepareNoteGroupBuilder(
|
||||||
|
searchParams: $searchParams,
|
||||||
|
user: $user,
|
||||||
|
notRead: $notRead,
|
||||||
|
beforeNumber: $beforeNumber,
|
||||||
|
);
|
||||||
|
|
||||||
|
$emailGroupBuilder = $this->prepareEmailGroupBuilder(
|
||||||
|
searchParams: $searchParams,
|
||||||
|
user: $user,
|
||||||
|
notRead: $notRead,
|
||||||
|
beforeNumber: $beforeNumber,
|
||||||
|
);
|
||||||
|
|
||||||
|
$restBuilder = $this->prepareRestBuilder(
|
||||||
|
searchParams: $searchParams,
|
||||||
|
user: $user,
|
||||||
|
notRead: $notRead,
|
||||||
|
beforeNumber: $beforeNumber,
|
||||||
|
);
|
||||||
|
|
||||||
|
return UnionBuilder::create()
|
||||||
|
->query($noteGroupBuilder->build())
|
||||||
|
->query($emailGroupBuilder->build())
|
||||||
|
->query($restBuilder->build())
|
||||||
|
->order(Notification::ATTR_NUMBER, Order::DESC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param RDBSelectBuilder<Notification>|SelectBuilder $builder
|
||||||
|
*/
|
||||||
|
private function applyRelatedAccess(RDBSelectBuilder|SelectBuilder $builder): void
|
||||||
|
{
|
||||||
|
$ignoreScopeList = $this->getIgnoreScopeList();
|
||||||
|
|
||||||
|
if ($ignoreScopeList === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder->where([
|
||||||
|
'OR' => [
|
||||||
|
Notification::ATTR_RELATED_PARENT_TYPE => null,
|
||||||
|
Notification::ATTR_RELATED_PARENT_TYPE . '!=' => $ignoreScopeList,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return EntityCollection<Notification>
|
||||||
|
*/
|
||||||
|
private function fetchAndPrepareCollectionFromUnion(Union $query): EntityCollection
|
||||||
|
{
|
||||||
|
$sth = $this->entityManager->getQueryExecutor()->execute($query);
|
||||||
|
|
||||||
|
/** @var EntityCollection<Notification> $collection */
|
||||||
|
$collection = $this->entityManager->getCollectionFactory()->create(Notification::ENTITY_TYPE);
|
||||||
|
|
||||||
|
while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$entity = $this->entityManager->getRDBRepositoryByClass(Notification::class)->getNew();
|
||||||
|
|
||||||
|
$entity->setMultiple($row);
|
||||||
|
$entity->setAsFetched();
|
||||||
|
|
||||||
|
$collection[] = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Selection[]
|
||||||
|
*/
|
||||||
|
private function getNullNoGroupSelections(): array
|
||||||
|
{
|
||||||
|
return array_map(function ($attribute) {
|
||||||
|
return Selection::create(Expr::value(null), $attribute);
|
||||||
|
}, $this->noGroupAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function prepareQueryBuilder(
|
||||||
|
User $user,
|
||||||
|
SearchParams $searchParams,
|
||||||
|
bool $notRead = false,
|
||||||
|
?string $beforeNumber = null,
|
||||||
|
): SelectBuilder {
|
||||||
|
|
||||||
|
$builder = $this->selectBuilderFactory
|
||||||
|
->create()
|
||||||
|
->from(Notification::ENTITY_TYPE)
|
||||||
|
->withSearchParams($searchParams)
|
||||||
|
->buildQueryBuilder()
|
||||||
|
->where([Notification::ATTR_USER_ID => $user->getId()])
|
||||||
|
->order(Notification::ATTR_NUMBER, SearchParams::ORDER_DESC);
|
||||||
|
|
||||||
|
if ($notRead) {
|
||||||
|
$builder->where([Notification::ATTR_READ => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($beforeNumber) {
|
||||||
|
$builder->where([Notification::ATTR_NUMBER . '<' => $beforeNumber]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applyRelatedAccess($builder);
|
||||||
|
|
||||||
|
if ($this->isActionGroupingEnabled()) {
|
||||||
|
$builder->where($this->getActionIdWhere($user->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function prepareNoteGroupBuilder(
|
||||||
|
SearchParams $searchParams,
|
||||||
|
User $user,
|
||||||
|
bool $notRead,
|
||||||
|
?string $beforeNumber,
|
||||||
|
): SelectBuilder {
|
||||||
|
|
||||||
|
$builder = $this->selectBuilderFactory
|
||||||
|
->create()
|
||||||
|
->from(Notification::ENTITY_TYPE)
|
||||||
|
->withSearchParams($searchParams)
|
||||||
|
->buildQueryBuilder()
|
||||||
|
->select([
|
||||||
|
Selection::create(
|
||||||
|
Expr::value(Notification::GROUP_TYPE_NOTE),
|
||||||
|
self::COLUMN_GROUP_TYPE
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::concat(
|
||||||
|
Expr::value(Notification::GROUP_TYPE_NOTE),
|
||||||
|
Expr::value('_'),
|
||||||
|
Expr::column(Notification::ATTR_RELATED_PARENT_TYPE),
|
||||||
|
Expr::value('_'),
|
||||||
|
Expr::column(Notification::ATTR_RELATED_PARENT_ID),
|
||||||
|
),
|
||||||
|
Attribute::ID
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::max(Expr::column(Notification::ATTR_NUMBER)),
|
||||||
|
Notification::ATTR_NUMBER
|
||||||
|
),
|
||||||
|
Notification::ATTR_RELATED_PARENT_ID,
|
||||||
|
Notification::ATTR_RELATED_PARENT_TYPE,
|
||||||
|
Selection::create(
|
||||||
|
Expr::switch(
|
||||||
|
Expr::greater(Expr::sum(Expr::not(Expr::column(Notification::ATTR_READ))), 0),
|
||||||
|
Expr::value(false),
|
||||||
|
Expr::value(true),
|
||||||
|
),
|
||||||
|
Notification::ATTR_READ
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::sum(
|
||||||
|
Expr::switch(
|
||||||
|
Expr::column(Notification::ATTR_READ),
|
||||||
|
Expr::value(0),
|
||||||
|
Expr::value(1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
self::COLUMN_GROUP_UNREAD_COUNT
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::max(Expr::column(Field::CREATED_AT)),
|
||||||
|
Field::CREATED_AT,
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::switch(
|
||||||
|
Expr::greater(
|
||||||
|
Expr::sum(
|
||||||
|
Expr::and(
|
||||||
|
Expr::not(Expr::column(Notification::ATTR_READ)),
|
||||||
|
Expr::column(Notification::FIELD_IS_FEATURED),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Notification::FIELD_IS_FEATURED
|
||||||
|
),
|
||||||
|
...$this->getNullNoGroupSelections(),
|
||||||
|
])
|
||||||
|
->where([
|
||||||
|
Notification::ATTR_RELATED_PARENT_ID . '!=' => null,
|
||||||
|
Notification::FIELD_TYPE => Notification::TYPE_NOTE,
|
||||||
|
])
|
||||||
|
->where([Notification::ATTR_USER_ID => $user->getId()])
|
||||||
|
->group(Notification::ATTR_RELATED_PARENT_ID)
|
||||||
|
->group(Notification::ATTR_RELATED_PARENT_TYPE)
|
||||||
|
->order(Notification::ATTR_NUMBER, Order::DESC);
|
||||||
|
|
||||||
|
if ($beforeNumber) {
|
||||||
|
$builder->having(
|
||||||
|
Cond::less(
|
||||||
|
Expr::max(Expr::column(Notification::ATTR_NUMBER)),
|
||||||
|
Expr::value($beforeNumber)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applyRelatedAccess($builder);
|
||||||
|
|
||||||
|
if ($notRead) {
|
||||||
|
$builder->where([Notification::ATTR_READ => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function prepareEmailGroupBuilder(
|
||||||
|
SearchParams $searchParams,
|
||||||
|
User $user,
|
||||||
|
bool $notRead,
|
||||||
|
?string $beforeNumber,
|
||||||
|
): SelectBuilder {
|
||||||
|
|
||||||
|
$builder = $this->selectBuilderFactory
|
||||||
|
->create()
|
||||||
|
->from(Notification::ENTITY_TYPE)
|
||||||
|
->withSearchParams($searchParams)
|
||||||
|
->buildQueryBuilder()
|
||||||
|
->select([
|
||||||
|
Selection::create(
|
||||||
|
Expr::value(Notification::GROUP_TYPE_EMAIL_RECEIVED),
|
||||||
|
self::COLUMN_GROUP_TYPE
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::concat(
|
||||||
|
Expr::value(Notification::GROUP_TYPE_EMAIL_RECEIVED),
|
||||||
|
Expr::value('_'),
|
||||||
|
),
|
||||||
|
Attribute::ID
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::max(Expr::column(Notification::ATTR_NUMBER)),
|
||||||
|
Notification::ATTR_NUMBER
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::value(null),
|
||||||
|
Notification::ATTR_RELATED_PARENT_ID,
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::value(null),
|
||||||
|
Notification::ATTR_RELATED_PARENT_TYPE,
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::switch(
|
||||||
|
Expr::greater(Expr::sum(Expr::not(Expr::column(Notification::ATTR_READ))), 0),
|
||||||
|
Expr::value(false),
|
||||||
|
Expr::value(true),
|
||||||
|
),
|
||||||
|
Notification::ATTR_READ
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::sum(
|
||||||
|
Expr::switch(
|
||||||
|
Expr::column(Notification::ATTR_READ),
|
||||||
|
Expr::value(0),
|
||||||
|
Expr::value(1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
self::COLUMN_GROUP_UNREAD_COUNT
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::max(Expr::column(Field::CREATED_AT)),
|
||||||
|
Field::CREATED_AT,
|
||||||
|
),
|
||||||
|
Selection::create(
|
||||||
|
Expr::value(null),
|
||||||
|
Notification::FIELD_IS_FEATURED,
|
||||||
|
),
|
||||||
|
...$this->getNullNoGroupSelections(),
|
||||||
|
])
|
||||||
|
->where([
|
||||||
|
Notification::FIELD_TYPE => Notification::TYPE_EMAIL_RECEIVED,
|
||||||
|
])
|
||||||
|
->where([Notification::ATTR_USER_ID => $user->getId()])
|
||||||
|
->group(Notification::FIELD_TYPE)
|
||||||
|
->order(Notification::ATTR_NUMBER, Order::DESC);
|
||||||
|
|
||||||
|
if ($beforeNumber) {
|
||||||
|
$builder->having(
|
||||||
|
Cond::less(
|
||||||
|
Expr::max(Expr::column(Notification::ATTR_NUMBER)),
|
||||||
|
Expr::value($beforeNumber)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($notRead) {
|
||||||
|
$builder->where([Notification::ATTR_READ => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws BadRequest
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function prepareRestBuilder(
|
||||||
|
SearchParams $searchParams,
|
||||||
|
User $user,
|
||||||
|
bool $notRead,
|
||||||
|
?string $beforeNumber,
|
||||||
|
): SelectBuilder {
|
||||||
|
|
||||||
|
$builder = $this->selectBuilderFactory
|
||||||
|
->create()
|
||||||
|
->from(Notification::ENTITY_TYPE)
|
||||||
|
->withSearchParams($searchParams)
|
||||||
|
->buildQueryBuilder()
|
||||||
|
->select([
|
||||||
|
Selection::create(
|
||||||
|
Expr::value(null),
|
||||||
|
self::COLUMN_GROUP_TYPE,
|
||||||
|
),
|
||||||
|
Attribute::ID,
|
||||||
|
Notification::ATTR_NUMBER,
|
||||||
|
Notification::ATTR_RELATED_PARENT_ID,
|
||||||
|
Notification::ATTR_RELATED_PARENT_TYPE,
|
||||||
|
Notification::ATTR_READ,
|
||||||
|
Selection::create(
|
||||||
|
Expr::switch(
|
||||||
|
Expr::column(Notification::ATTR_READ),
|
||||||
|
Expr::value(0),
|
||||||
|
Expr::value(1),
|
||||||
|
),
|
||||||
|
self::COLUMN_GROUP_UNREAD_COUNT
|
||||||
|
),
|
||||||
|
Field::CREATED_AT,
|
||||||
|
Selection::create(
|
||||||
|
Expr::value(null),
|
||||||
|
Notification::FIELD_IS_FEATURED,
|
||||||
|
),
|
||||||
|
...$this->noGroupAttributes,
|
||||||
|
])
|
||||||
|
->where([
|
||||||
|
[
|
||||||
|
'OR' => [
|
||||||
|
Notification::FIELD_TYPE . '!=' => Notification::TYPE_NOTE,
|
||||||
|
Notification::ATTR_RELATED_PARENT_ID => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
Notification::FIELD_TYPE . '!=' => Notification::TYPE_EMAIL_RECEIVED,
|
||||||
|
])
|
||||||
|
->where([Notification::ATTR_USER_ID => $user->getId()])
|
||||||
|
->order(Notification::ATTR_NUMBER, Order::DESC);
|
||||||
|
|
||||||
|
if ($notRead) {
|
||||||
|
$builder->where([Notification::ATTR_READ => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($beforeNumber) {
|
||||||
|
$builder->where([Notification::ATTR_NUMBER . '<' => $beforeNumber]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ use Espo\Modules\Crm\Entities\CaseObj;
|
|||||||
use Espo\ORM\EntityManager;
|
use Espo\ORM\EntityManager;
|
||||||
use Espo\ORM\Name\Attribute;
|
use Espo\ORM\Name\Attribute;
|
||||||
use Espo\Tools\Notification\HookProcessor\Params;
|
use Espo\Tools\Notification\HookProcessor\Params;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
class Service
|
class Service
|
||||||
{
|
{
|
||||||
@@ -91,7 +92,7 @@ class Service
|
|||||||
$collection = $this->entityManager->getCollectionFactory()->create();
|
$collection = $this->entityManager->getCollectionFactory()->create();
|
||||||
|
|
||||||
$users = $this->entityManager
|
$users = $this->entityManager
|
||||||
->getRDBRepository(User::ENTITY_TYPE)
|
->getRDBRepositoryByClass(User::class)
|
||||||
->select([
|
->select([
|
||||||
Attribute::ID,
|
Attribute::ID,
|
||||||
User::ATTR_TYPE,
|
User::ATTR_TYPE,
|
||||||
@@ -124,10 +125,11 @@ class Service
|
|||||||
|
|
||||||
$actionId = $params?->actionId;
|
$actionId = $params?->actionId;
|
||||||
|
|
||||||
if (
|
$isFeatured = false;
|
||||||
in_array($note->getType(), [Note::TYPE_ASSIGN, Note::TYPE_CREATE]) &&
|
|
||||||
($note->getData()->assignedUserId ?? null) === $user->getId()
|
if ($this->isUserTargetAssignee($note, $user)) {
|
||||||
) {
|
$isFeatured = true;
|
||||||
|
|
||||||
// Do not group notifications about assignment.
|
// Do not group notifications about assignment.
|
||||||
$actionId = null;
|
$actionId = null;
|
||||||
}
|
}
|
||||||
@@ -137,7 +139,7 @@ class Service
|
|||||||
$notification
|
$notification
|
||||||
->set(Attribute::ID, $this->idGenerator->generate())
|
->set(Attribute::ID, $this->idGenerator->generate())
|
||||||
->set(Field::CREATED_AT, $now)
|
->set(Field::CREATED_AT, $now)
|
||||||
->setData(['noteId' => $note->getId()])
|
->setData([Notification::DATE_ATTR_NOTE_ID => $note->getId()])
|
||||||
->setType(Notification::TYPE_NOTE)
|
->setType(Notification::TYPE_NOTE)
|
||||||
->setUserId($user->getId())
|
->setUserId($user->getId())
|
||||||
->setRelated(LinkParent::fromEntity($note))
|
->setRelated(LinkParent::fromEntity($note))
|
||||||
@@ -145,7 +147,8 @@ class Service
|
|||||||
$note->getParentType() && $note->getParentId() ?
|
$note->getParentType() && $note->getParentId() ?
|
||||||
LinkParent::create($note->getParentType(), $note->getParentId()) : null
|
LinkParent::create($note->getParentType(), $note->getParentId()) : null
|
||||||
)
|
)
|
||||||
->setActionId($actionId);
|
->setActionId($actionId)
|
||||||
|
->setIsFeatured($isFeatured);
|
||||||
|
|
||||||
$collection[] = $notification;
|
$collection[] = $notification;
|
||||||
}
|
}
|
||||||
@@ -184,4 +187,37 @@ class Service
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function isUserTargetAssignee(Note $note, User $user): bool
|
||||||
|
{
|
||||||
|
if (!in_array($note->getType(), [Note::TYPE_ASSIGN, Note::TYPE_CREATE])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$noteData = $note->getData();
|
||||||
|
|
||||||
|
if (($noteData->{Note::DATA_ATTR_ASSIGNED_USER_ID} ?? null) === $user->getId()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignedUsers = $noteData->{Note::DATA_ATTR_ASSIGNED_USERS} ??
|
||||||
|
$noteData->{Note::DATA_ATTR_ADDED_ASSIGNED_USERS} ?? null;
|
||||||
|
|
||||||
|
if (!is_array($assignedUsers)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($assignedUsers as $item) {
|
||||||
|
if (!$item instanceof stdClass) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($item->{Attribute::ID} ?? null) === $user->getId()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
namespace Espo\Tools\Stream;
|
namespace Espo\Tools\Stream;
|
||||||
|
|
||||||
|
use Espo\Core\Acl\AssignmentChecker\Helper as AsssignmentHelper;
|
||||||
use Espo\Core\Field\DateTime;
|
use Espo\Core\Field\DateTime;
|
||||||
use Espo\Core\Field\LinkMultiple;
|
use Espo\Core\Field\LinkMultiple;
|
||||||
use Espo\Core\Field\LinkParent;
|
use Espo\Core\Field\LinkParent;
|
||||||
@@ -105,7 +106,8 @@ class Service
|
|||||||
private SelectBuilderFactory $selectBuilderFactory,
|
private SelectBuilderFactory $selectBuilderFactory,
|
||||||
private UserAclManagerProvider $userAclManagerProvider,
|
private UserAclManagerProvider $userAclManagerProvider,
|
||||||
private RecordServiceContainer $recordServiceContainer,
|
private RecordServiceContainer $recordServiceContainer,
|
||||||
private SystemUser $systemUser
|
private SystemUser $systemUser,
|
||||||
|
private AsssignmentHelper $assignmentHelper,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private function getStatusField(string $entityType): ?string
|
private function getStatusField(string $entityType): ?string
|
||||||
@@ -593,27 +595,30 @@ class Service
|
|||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
|
|
||||||
if ($entity->get('assignedUserId')) {
|
if (
|
||||||
$this->loadAssignedUserName($entity);
|
|
||||||
|
|
||||||
$data['assignedUserId'] = $entity->get('assignedUserId');
|
|
||||||
$data['assignedUserName'] = $entity->get('assignedUserName');
|
|
||||||
} else if (
|
|
||||||
$entity instanceof CoreEntity &&
|
$entity instanceof CoreEntity &&
|
||||||
$entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS) &&
|
|
||||||
$entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS) !== [] &&
|
|
||||||
// Exclude for Email as the assignedUsers serves not for direct assignment.
|
// Exclude for Email as the assignedUsers serves not for direct assignment.
|
||||||
$entity->getEntityType() !== Email::ENTITY_TYPE
|
$entity->getEntityType() !== Email::ENTITY_TYPE &&
|
||||||
|
$this->assignmentHelper->hasAssignedUsersField($entity->getEntityType()) &&
|
||||||
|
$entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS) !== []
|
||||||
) {
|
) {
|
||||||
/** @var LinkMultiple $users */
|
/** @var LinkMultiple $users */
|
||||||
$users = $entity->getValueObject(self::FIELD_ASSIGNED_USERS);
|
$users = $entity->getValueObject(self::FIELD_ASSIGNED_USERS);
|
||||||
|
|
||||||
$data['assignedUsers'] = array_map(function ($it) {
|
$data[Note::DATA_ATTR_ASSIGNED_USERS] = array_map(function ($it) {
|
||||||
return [
|
return [
|
||||||
Attribute::ID => $it->getId(),
|
Attribute::ID => $it->getId(),
|
||||||
'name' => $it->getName(),
|
Field::NAME => $it->getName(),
|
||||||
];
|
];
|
||||||
}, $users->getList());
|
}, $users->getList());
|
||||||
|
} else if (
|
||||||
|
$this->assignmentHelper->hasAssignedUserField($entity->getEntityType()) &&
|
||||||
|
$entity->get(Field::ASSIGNED_USER . 'Id')
|
||||||
|
) {
|
||||||
|
$this->loadAssignedUserName($entity);
|
||||||
|
|
||||||
|
$data[Note::DATA_ATTR_ASSIGNED_USER_ID] = $entity->get(Field::ASSIGNED_USER . 'Id');
|
||||||
|
$data[Note::DATA_ATTR_ASSIGNED_USER_NAME] = $entity->get(Field::ASSIGNED_USER . 'Name');
|
||||||
}
|
}
|
||||||
|
|
||||||
$field = $this->getStatusField($entityType);
|
$field = $this->getStatusField($entityType);
|
||||||
@@ -622,12 +627,12 @@ class Service
|
|||||||
$value = $entity->get($field);
|
$value = $entity->get($field);
|
||||||
|
|
||||||
if ($value) {
|
if ($value) {
|
||||||
$data['statusValue'] = $value;
|
$data[Note::DATA_ATTR_STATUS_VALUE] = $value;
|
||||||
$data['statusField'] = $field;
|
$data[Note::DATA_ATTR_STATUS_FIELD] = $field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$note->set('data', (object) $data);
|
$note->setData($data);
|
||||||
|
|
||||||
$noteOptions = [];
|
$noteOptions = [];
|
||||||
|
|
||||||
@@ -915,8 +920,8 @@ class Service
|
|||||||
$note->setParent(LinkParent::fromEntity($entity));
|
$note->setParent(LinkParent::fromEntity($entity));
|
||||||
|
|
||||||
$note->setData([
|
$note->setData([
|
||||||
'fields' => $updatedFieldList,
|
Note::DATA_ATTR_FIELDS => $updatedFieldList,
|
||||||
'attributes' => [
|
Note::DATA_ATTR_ATTRIBUTES => [
|
||||||
'was' => (object) $was,
|
'was' => (object) $was,
|
||||||
'became' => (object) $became,
|
'became' => (object) $became,
|
||||||
],
|
],
|
||||||
@@ -1160,9 +1165,10 @@ class Service
|
|||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
$entity instanceof CoreEntity &&
|
$entity instanceof CoreEntity &&
|
||||||
$entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS) &&
|
$entity->getEntityType() !== Email::ENTITY_TYPE &&
|
||||||
// Exclude for Email as the assignedUsers serves not for direct assignment.
|
// Exclude for Email as the assignedUsers serves not for direct assignment.
|
||||||
$entity->getEntityType() !== Email::ENTITY_TYPE
|
$this->assignmentHelper->hasAssignedUsersField($entity->getEntityType()) &&
|
||||||
|
$entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS) !== []
|
||||||
) {
|
) {
|
||||||
$data = [];
|
$data = [];
|
||||||
|
|
||||||
@@ -1179,17 +1185,17 @@ class Service
|
|||||||
$removedIds = array_values(array_diff($prevIds, $newIds));
|
$removedIds = array_values(array_diff($prevIds, $newIds));
|
||||||
$names = array_merge($prevNames, $newNames);
|
$names = array_merge($prevNames, $newNames);
|
||||||
|
|
||||||
$data['addedAssignedUsers'] = array_map(function ($id) use ($names) {
|
$data[Note::DATA_ATTR_ADDED_ASSIGNED_USERS] = array_map(function ($id) use ($names) {
|
||||||
return [
|
return [
|
||||||
'id' => $id,
|
Attribute::ID => $id,
|
||||||
'name' => $names[$id] ?? null,
|
Field::NAME => $names[$id] ?? null,
|
||||||
];
|
];
|
||||||
}, $addedIds);
|
}, $addedIds);
|
||||||
|
|
||||||
$data['removedAssignedUsers'] = array_map(function ($id) use ($names) {
|
$data[Note::DATA_ATTR_REMOVED_ASSIGNED_USERS] = array_map(function ($id) use ($names) {
|
||||||
return [
|
return [
|
||||||
'id' => $id,
|
Attribute::ID => $id,
|
||||||
'name' => $names[$id] ?? null,
|
Field::NAME => $names[$id] ?? null,
|
||||||
];
|
];
|
||||||
}, $removedIds);
|
}, $removedIds);
|
||||||
|
|
||||||
@@ -1198,18 +1204,22 @@ class Service
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($entity->get('assignedUserId')) {
|
if (
|
||||||
|
$this->assignmentHelper->hasAssignedUserField($entity->getEntityType()) &&
|
||||||
|
$entity->get(Field::ASSIGNED_USER . 'Id')
|
||||||
|
) {
|
||||||
$this->loadAssignedUserName($entity);
|
$this->loadAssignedUserName($entity);
|
||||||
|
|
||||||
$note->setData([
|
$note->setData([
|
||||||
'assignedUserId' => $entity->get('assignedUserId'),
|
Note::DATA_ATTR_ASSIGNED_USER_ID => $entity->get(Field::ASSIGNED_USER . 'Id'),
|
||||||
'assignedUserName' => $entity->get('assignedUserName'),
|
Note::DATA_ATTR_ASSIGNED_USER_NAME => $entity->get(Field::ASSIGNED_USER . 'Name'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$note->setData(['assignedUserId' => null]);
|
|
||||||
|
$note->setData([Note::DATA_ATTR_ASSIGNED_USER_ID => null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="notification-container">{{{notification}}}</div>
|
<div class="notification-container">{{{notification}}}</div>
|
||||||
{{#if hasGrouped}}
|
{{#if hasGrouped}}
|
||||||
<div class="notification-grouped">
|
<div class="notification-grouped list-container-panel">
|
||||||
{{#if isGroupExpanded}}
|
{{#if isGroupExpanded}}
|
||||||
{{{groupedList}}}
|
{{{groupedList}}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ import Collection from 'collection';
|
|||||||
|
|
||||||
class NoteCollection extends Collection {
|
class NoteCollection extends Collection {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
paginationByNumber = false
|
paginationByNumber = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
import NoteCollection from 'collections/note';
|
||||||
|
|
||||||
|
export default class NotificationCollection extends NoteCollection {
|
||||||
|
|
||||||
|
paginationByNumber = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
import Model from 'model';
|
||||||
|
|
||||||
|
export default class Notification extends Model {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
constructor(attributes, options) {
|
||||||
|
super(attributes, options);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!options.url &&
|
||||||
|
this.attributes.groupType
|
||||||
|
) {
|
||||||
|
this.urlRoot = 'Notification/group';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -568,6 +568,7 @@ class NotificationBadgeView extends View {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.listenTo(view, 'collection-fetched', () => {
|
this.listenTo(view, 'collection-fetched', () => {
|
||||||
|
console.log(1);
|
||||||
this.checkUpdates();
|
this.checkUpdates();
|
||||||
this.broadcastNotificationsRead();
|
this.broadcastNotificationsRead();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
import BaseFieldView from 'views/fields/base';
|
import BaseFieldView from 'views/fields/base';
|
||||||
import NotificationListRecordView from 'views/notification/record/list';
|
import NotificationListRecordView from 'views/notification/record/list';
|
||||||
|
import NotificationPanelView from 'views/notification/panel';
|
||||||
|
|
||||||
class NotificationContainerFieldView extends BaseFieldView {
|
class NotificationContainerFieldView extends BaseFieldView {
|
||||||
|
|
||||||
@@ -59,13 +60,18 @@ class NotificationContainerFieldView extends BaseFieldView {
|
|||||||
isGroupExpanded = false
|
isGroupExpanded = false
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
const count = this.model.attributes.groupedCount ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasGrouped: (this.model.attributes.groupedCount ?? 0) > 1,
|
hasGrouped: count > 1 || count < 0,
|
||||||
isGroupExpanded: this.isGroupExpanded,
|
isGroupExpanded: this.isGroupExpanded,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
if (this.model.attributes.groupType) {
|
||||||
|
this.wait(this.processGroup());
|
||||||
|
} else {
|
||||||
switch (this.model.attributes.type) {
|
switch (this.model.attributes.type) {
|
||||||
case 'Note':
|
case 'Note':
|
||||||
this.processNote(this.model.attributes.noteData);
|
this.processNote(this.model.attributes.noteData);
|
||||||
@@ -80,6 +86,7 @@ class NotificationContainerFieldView extends BaseFieldView {
|
|||||||
default:
|
default:
|
||||||
this.process();
|
this.process();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.addActionHandler('showGrouped', () => this.showGrouped());
|
this.addActionHandler('showGrouped', () => this.showGrouped());
|
||||||
}
|
}
|
||||||
@@ -111,6 +118,32 @@ class NotificationContainerFieldView extends BaseFieldView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async processGroup() {
|
||||||
|
const groupType = this.model.attributes.groupType;
|
||||||
|
|
||||||
|
let viewName;
|
||||||
|
|
||||||
|
if (groupType === 'Note') {
|
||||||
|
viewName = 'views/notification/items/group-note';
|
||||||
|
} else if (groupType === 'EmailReceived') {
|
||||||
|
viewName = 'views/notification/items/group-email-received';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!viewName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentSelector = this.options.containerSelector ?? this.getSelector();
|
||||||
|
|
||||||
|
await this.createView('notification', viewName, {
|
||||||
|
model: this.model,
|
||||||
|
fullSelector: `${parentSelector} li[data-id="${this.model.id}"]`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {Record} data
|
* @param {Record} data
|
||||||
@@ -140,6 +173,7 @@ class NotificationContainerFieldView extends BaseFieldView {
|
|||||||
fullSelector: `${parentSelector} li[data-id="${this.model.id}"] .cell[data-name="data"]`,
|
fullSelector: `${parentSelector} li[data-id="${this.model.id}"] .cell[data-name="data"]`,
|
||||||
onlyContent: true,
|
onlyContent: true,
|
||||||
isNotification: true,
|
isNotification: true,
|
||||||
|
isInGroup: this.options.isInGroup ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.wait(false);
|
this.wait(false);
|
||||||
@@ -183,7 +217,14 @@ class NotificationContainerFieldView extends BaseFieldView {
|
|||||||
async showGrouped() {
|
async showGrouped() {
|
||||||
const collection = await this.getCollectionFactory().create('Notification');
|
const collection = await this.getCollectionFactory().create('Notification');
|
||||||
|
|
||||||
|
if (this.model.attributes.groupType) {
|
||||||
|
collection.url = `Notification/group?type=${this.model.attributes.groupType}&id=` +
|
||||||
|
this.model.id;
|
||||||
|
|
||||||
|
collection.maxSize = this.getConfig().get('recordsPerPageSmall');
|
||||||
|
} else {
|
||||||
collection.url = `Notification/${this.model.id}/group`;
|
collection.url = `Notification/${this.model.id}/group`;
|
||||||
|
}
|
||||||
|
|
||||||
const button = this.element.querySelector('a[data-action="showGrouped"]');
|
const button = this.element.querySelector('a[data-action="showGrouped"]');
|
||||||
|
|
||||||
@@ -221,6 +262,9 @@ class NotificationContainerFieldView extends BaseFieldView {
|
|||||||
{
|
{
|
||||||
name: 'data',
|
name: 'data',
|
||||||
view: 'views/notification/fields/container',
|
view: 'views/notification/fields/container',
|
||||||
|
options: {
|
||||||
|
isInGroup: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -235,6 +279,20 @@ class NotificationContainerFieldView extends BaseFieldView {
|
|||||||
await this.assignView('groupedList', view);
|
await this.assignView('groupedList', view);
|
||||||
|
|
||||||
await this.reRender();
|
await this.reRender();
|
||||||
|
|
||||||
|
let viewPointer = this;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
viewPointer = viewPointer.getParentView();
|
||||||
|
|
||||||
|
if (!viewPointer || viewPointer instanceof NotificationPanelView) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewPointer instanceof NotificationPanelView) {
|
||||||
|
viewPointer.trigger('collection-fetched');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,12 +41,7 @@ class AssignNotificationItemView extends BaseNotificationItemView {
|
|||||||
|
|
||||||
this.messageData['entityType'] = this.translateEntityType(data.entityType);
|
this.messageData['entityType'] = this.translateEntityType(data.entityType);
|
||||||
|
|
||||||
this.messageData['entity'] =
|
this.messageData['entity'] = 'field:related';
|
||||||
$('<a>')
|
|
||||||
.attr('href', '#' + data.entityType + '/view/' + data.entityId)
|
|
||||||
.attr('data-id', data.entityId)
|
|
||||||
.attr('data-scope', data.entityType)
|
|
||||||
.text(data.entityName);
|
|
||||||
|
|
||||||
this.messageData['user'] =
|
this.messageData['user'] =
|
||||||
$('<a>')
|
$('<a>')
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
import BaseNotificationItemView from 'views/notification/items/base';
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
export default class GroupNoteNotificationItemView extends BaseNotificationItemView {
|
||||||
|
|
||||||
|
// language=Handlebars
|
||||||
|
templateContent = `
|
||||||
|
<div class="stream-head-container">
|
||||||
|
<div class="pull-left stream-head-left-icon-container">
|
||||||
|
<span
|
||||||
|
class="far fa-envelope text-soft icon"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="stream-head-text-container">
|
||||||
|
<span class="text-muted message">{{{message}}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stream-date-container">
|
||||||
|
<span class="text-muted small">{{{createdAt}}}</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
...super.data(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const newCount = this.model.attributes.groupedUnreadCount ?? 0;
|
||||||
|
|
||||||
|
this.messageData['number'] = newCount.toString();
|
||||||
|
|
||||||
|
this.messageName = 'groupEmailsReceivedNew';
|
||||||
|
|
||||||
|
if (newCount === 0) {
|
||||||
|
this.messageName = 'groupEmailsReceived';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* 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.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
import BaseNotificationItemView from 'views/notification/items/base';
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
export default class GroupNoteNotificationItemView extends BaseNotificationItemView {
|
||||||
|
|
||||||
|
// language=Handlebars
|
||||||
|
templateContent = `
|
||||||
|
<div class="stream-head-container">
|
||||||
|
{{#if iconClass}}
|
||||||
|
<div class="pull-left stream-head-left-icon-container">
|
||||||
|
<span
|
||||||
|
class=" {{iconClass}} text-muted action icon"
|
||||||
|
style="
|
||||||
|
cursor: pointer;
|
||||||
|
{{#if color}} color: {{color}}; {{/if}}
|
||||||
|
"
|
||||||
|
title="{{translate 'View'}}"
|
||||||
|
data-action="quickView"
|
||||||
|
data-id="{{relatedParentId}}"
|
||||||
|
data-scope="{{relatedParentType}}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<div class="stream-head-text-container">
|
||||||
|
<span class="text-muted message">{{{message}}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{~#if isFeatured~}}
|
||||||
|
<div class="stream-post-container">
|
||||||
|
<span class="label label-default">{{translate 'Assigned'}}</span>
|
||||||
|
</div>
|
||||||
|
{{~/if~}}
|
||||||
|
<div class="stream-date-container">
|
||||||
|
<span class="text-muted small">{{{createdAt}}}</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const relatedParentType = this.model.attributes.relatedParentType
|
||||||
|
const iconClass = this.getMetadata().get(`clientDefs.${relatedParentType}.iconClass`);
|
||||||
|
const color = this.getMetadata().get(`clientDefs.${relatedParentType}.color`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...super.data(),
|
||||||
|
relatedParentId: this.model.attributes.relatedParentId,
|
||||||
|
relatedParentType: this.model.attributes.relatedParentType,
|
||||||
|
isFeatured: this.model.attributes.isFeatured,
|
||||||
|
iconClass,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const relatedParentType = this.model.attributes.relatedParentType;
|
||||||
|
|
||||||
|
if (relatedParentType) {
|
||||||
|
this.messageData['entityType'] = this.translateEntityType(relatedParentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageData['entity'] = 'field:relatedParent';
|
||||||
|
|
||||||
|
const newCount = this.model.attributes.groupedUnreadCount ?? 0;
|
||||||
|
|
||||||
|
this.messageData['number'] = newCount.toString();
|
||||||
|
|
||||||
|
this.messageName = newCount > 1 ? 'groupUpdatesMultiple' : 'groupUpdatesOne';
|
||||||
|
|
||||||
|
if (newCount === 0) {
|
||||||
|
this.messageName = 'groupUpdates';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,14 +102,8 @@ export default class UserReactionNotificationItemView extends BaseNotificationIt
|
|||||||
this.messageData['user'] = userElement;
|
this.messageData['user'] = userElement;
|
||||||
|
|
||||||
if (relatedParentId && relatedParentType) {
|
if (relatedParentId && relatedParentType) {
|
||||||
const relatedParentElement = document.createElement('a');
|
|
||||||
relatedParentElement.href = `#${relatedParentType}/view/${relatedParentId}`;
|
|
||||||
relatedParentElement.dataset.id = relatedParentId;
|
|
||||||
relatedParentElement.dataset.scope = relatedParentType;
|
|
||||||
relatedParentElement.textContent = data.entityName || relatedParentType;
|
|
||||||
|
|
||||||
this.messageData['entityType'] = this.translateEntityType(relatedParentType);
|
this.messageData['entityType'] = this.translateEntityType(relatedParentType);
|
||||||
this.messageData['entity'] = relatedParentElement;
|
this.messageData['entity'] = 'field:relatedParent';
|
||||||
|
|
||||||
this.messageName = 'userPostInParentReaction';
|
this.messageName = 'userPostInParentReaction';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,8 +108,21 @@ class NotificationListRecordView extends ListExpandedRecordView {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
showNewRecords() {
|
showNewRecords() {
|
||||||
|
if (this.isGroupingEnabled()) {
|
||||||
|
return this.collection.fetch();
|
||||||
|
}
|
||||||
|
|
||||||
return this.collection.fetchNew();
|
return this.collection.fetchNew();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo Preferences?
|
||||||
|
* @private
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isGroupingEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationListRecordView;
|
export default NotificationListRecordView;
|
||||||
|
|||||||
@@ -243,6 +243,10 @@ class NoteStreamView extends View {
|
|||||||
id = this.model.attributes.parentId;
|
id = this.model.attributes.parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.isInGroup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isThis && this.parentModel && scope === this.parentModel.entityType) {
|
if (this.isThis && this.parentModel && scope === this.parentModel.entityType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ class MentionInPostNoteStreamView extends NoteStreamView {
|
|||||||
this.messageName = 'mentionInPostTarget';
|
this.messageName = 'mentionInPostTarget';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isUserStream || this.options.userId !== this.getUser().id) {
|
if (
|
||||||
|
(!this.options.isNotification) &&
|
||||||
|
(!this.isUserStream || this.options.userId !== this.getUser().id)
|
||||||
|
) {
|
||||||
this.createMessage();
|
this.createMessage();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1421,13 +1421,13 @@ section {
|
|||||||
.notification-grouped > .list > .list-group {
|
.notification-grouped > .list > .list-group {
|
||||||
> li {
|
> li {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-top-left-radius: var(--border-radius);
|
//border-top-left-radius: var(--border-radius);
|
||||||
border-top-right-radius: var(--border-radius);
|
//border-top-right-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom-left-radius: var(--border-radius);
|
//border-bottom-left-radius: var(--border-radius);
|
||||||
border-bottom-right-radius: var(--border-radius);
|
//border-bottom-right-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2147,7 +2147,9 @@ td > span.color-icon {
|
|||||||
top: var(--minus-1px);
|
top: var(--minus-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
padding-left: var(--29px);
|
padding-left: var(--29px);
|
||||||
|
}
|
||||||
|
|
||||||
> .icon:first-child {
|
> .icon:first-child {
|
||||||
min-width: var(--16px);
|
min-width: var(--16px);
|
||||||
@@ -2236,6 +2238,11 @@ td > span.color-icon {
|
|||||||
|
|
||||||
.stream-head-container {
|
.stream-head-container {
|
||||||
margin-bottom: var(--4px);
|
margin-bottom: var(--4px);
|
||||||
|
|
||||||
|
> .stream-head-left-icon-container {
|
||||||
|
width: var(--26px);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-head-container .internal-badge {
|
.stream-head-container .internal-badge {
|
||||||
|
|||||||
Reference in New Issue
Block a user