notification grouping

This commit is contained in:
Yurii
2026-03-09 14:17:46 +02:00
parent 1877aeecb0
commit 49f7ac30ca
32 changed files with 1514 additions and 130 deletions
@@ -1,6 +1,6 @@
<div class="notification-container">{{{notification}}}</div>
{{#if hasGrouped}}
<div class="notification-grouped">
<div class="notification-grouped list-container-panel">
{{#if isGroupExpanded}}
{{{groupedList}}}
{{else}}
+3
View File
@@ -32,6 +32,9 @@ import Collection from 'collection';
class NoteCollection extends Collection {
/**
* @type {boolean}
*/
paginationByNumber = false
/**
+34
View File
@@ -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
}
+46
View File
@@ -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';
}
}
}
+1
View File
@@ -568,6 +568,7 @@ class NotificationBadgeView extends View {
});
this.listenTo(view, 'collection-fetched', () => {
console.log(1);
this.checkUpdates();
this.broadcastNotificationsRead();
});
@@ -28,6 +28,7 @@
import BaseFieldView from 'views/fields/base';
import NotificationListRecordView from 'views/notification/record/list';
import NotificationPanelView from 'views/notification/panel';
class NotificationContainerFieldView extends BaseFieldView {
@@ -59,26 +60,32 @@ class NotificationContainerFieldView extends BaseFieldView {
isGroupExpanded = false
data() {
const count = this.model.attributes.groupedCount ?? 0;
return {
hasGrouped: (this.model.attributes.groupedCount ?? 0) > 1,
hasGrouped: count > 1 || count < 0,
isGroupExpanded: this.isGroupExpanded,
};
}
setup() {
switch (this.model.attributes.type) {
case 'Note':
this.processNote(this.model.attributes.noteData);
if (this.model.attributes.groupType) {
this.wait(this.processGroup());
} else {
switch (this.model.attributes.type) {
case 'Note':
this.processNote(this.model.attributes.noteData);
break;
break;
case 'MentionInPost':
this.processMentionInPost(this.model.attributes.noteData);
case 'MentionInPost':
this.processMentionInPost(this.model.attributes.noteData);
break;
break;
default:
this.process();
default:
this.process();
}
}
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
* @param {Record} data
@@ -140,6 +173,7 @@ class NotificationContainerFieldView extends BaseFieldView {
fullSelector: `${parentSelector} li[data-id="${this.model.id}"] .cell[data-name="data"]`,
onlyContent: true,
isNotification: true,
isInGroup: this.options.isInGroup ?? false,
});
this.wait(false);
@@ -183,7 +217,14 @@ class NotificationContainerFieldView extends BaseFieldView {
async showGrouped() {
const collection = await this.getCollectionFactory().create('Notification');
collection.url = `Notification/${this.model.id}/group`;
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`;
}
const button = this.element.querySelector('a[data-action="showGrouped"]');
@@ -221,6 +262,9 @@ class NotificationContainerFieldView extends BaseFieldView {
{
name: 'data',
view: 'views/notification/fields/container',
options: {
isInGroup: true,
},
},
],
],
@@ -235,6 +279,20 @@ class NotificationContainerFieldView extends BaseFieldView {
await this.assignView('groupedList', view);
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['entity'] =
$('<a>')
.attr('href', '#' + data.entityType + '/view/' + data.entityId)
.attr('data-id', data.entityId)
.attr('data-scope', data.entityType)
.text(data.entityName);
this.messageData['entity'] = 'field:related';
this.messageData['user'] =
$('<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;
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['entity'] = relatedParentElement;
this.messageData['entity'] = 'field:relatedParent';
this.messageName = 'userPostInParentReaction';
}
@@ -108,8 +108,21 @@ class NotificationListRecordView extends ListExpandedRecordView {
* @return {Promise}
*/
showNewRecords() {
if (this.isGroupingEnabled()) {
return this.collection.fetch();
}
return this.collection.fetchNew();
}
/**
* @todo Preferences?
* @private
* @return {boolean}
*/
isGroupingEnabled() {
return true;
}
}
export default NotificationListRecordView;
+4
View File
@@ -243,6 +243,10 @@ class NoteStreamView extends View {
id = this.model.attributes.parentId;
}
if (this.options.isInGroup) {
return null;
}
if (this.isThis && this.parentModel && scope === this.parentModel.entityType) {
return null;
}
@@ -58,7 +58,10 @@ class MentionInPostNoteStreamView extends NoteStreamView {
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();
return;