diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/components/notification_list | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/notification_list')
6 files changed, 1197 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/notification_list/notification_list.js b/addons/mail/static/src/components/notification_list/notification_list.js new file mode 100644 index 00000000..33737ba4 --- /dev/null +++ b/addons/mail/static/src/components/notification_list/notification_list.js @@ -0,0 +1,226 @@ +odoo.define('mail/static/src/components/notification_list/notification_list.js', function (require) { +'use strict'; + +const components = { + NotificationGroup: require('mail/static/src/components/notification_group/notification_group.js'), + NotificationRequest: require('mail/static/src/components/notification_request/notification_request.js'), + ThreadNeedactionPreview: require('mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js'), + ThreadPreview: require('mail/static/src/components/thread_preview/thread_preview.js'), +}; +const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { Component } = owl; + +class NotificationList extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useShouldUpdateBasedOnProps(); + this.storeProps = useStore((...args) => this._useStoreSelector(...args), { + compareDepth: { + // list + notification object created in useStore + notifications: 2, + }, + }); + } + + mounted() { + this._loadPreviews(); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {Object[]} + */ + get notifications() { + const { notifications } = this.storeProps; + return notifications; + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Load previews of given thread. Basically consists of fetching all missing + * last messages of each thread. + * + * @private + */ + async _loadPreviews() { + const threads = this.notifications + .filter(notification => notification.thread && notification.thread.exists()) + .map(notification => notification.thread); + this.env.models['mail.thread'].loadPreviews(threads); + } + + /** + * @private + * @param {Object} props + */ + _useStoreSelector(props) { + const threads = this._useStoreSelectorThreads(props); + let threadNeedactionNotifications = []; + if (props.filter === 'all') { + // threads with needactions + threadNeedactionNotifications = this.env.models['mail.thread'] + .all(t => t.model !== 'mail.box' && t.needactionMessagesAsOriginThread.length > 0) + .sort((t1, t2) => { + if (t1.needactionMessagesAsOriginThread.length > 0 && t2.needactionMessagesAsOriginThread.length === 0) { + return -1; + } + if (t1.needactionMessagesAsOriginThread.length === 0 && t2.needactionMessagesAsOriginThread.length > 0) { + return 1; + } + if (t1.lastNeedactionMessageAsOriginThread && t2.lastNeedactionMessageAsOriginThread) { + return t1.lastNeedactionMessageAsOriginThread.date.isBefore(t2.lastNeedactionMessageAsOriginThread.date) ? 1 : -1; + } + if (t1.lastNeedactionMessageAsOriginThread) { + return -1; + } + if (t2.lastNeedactionMessageAsOriginThread) { + return 1; + } + return t1.id < t2.id ? -1 : 1; + }) + .map(thread => { + return { + thread, + type: 'thread_needaction', + uniqueId: thread.localId + '_needaction', + }; + }); + } + // thread notifications + const threadNotifications = threads + .sort((t1, t2) => { + if (t1.localMessageUnreadCounter > 0 && t2.localMessageUnreadCounter === 0) { + return -1; + } + if (t1.localMessageUnreadCounter === 0 && t2.localMessageUnreadCounter > 0) { + return 1; + } + if (t1.lastMessage && t2.lastMessage) { + return t1.lastMessage.date.isBefore(t2.lastMessage.date) ? 1 : -1; + } + if (t1.lastMessage) { + return -1; + } + if (t2.lastMessage) { + return 1; + } + return t1.id < t2.id ? -1 : 1; + }) + .map(thread => { + return { + thread, + type: 'thread', + uniqueId: thread.localId, + }; + }); + let notifications = threadNeedactionNotifications.concat(threadNotifications); + if (props.filter === 'all') { + const notificationGroups = this.env.messaging.notificationGroupManager.groups; + notifications = Object.values(notificationGroups) + .sort((group1, group2) => + group1.date.isAfter(group2.date) ? -1 : 1 + ).map(notificationGroup => { + return { + notificationGroup, + uniqueId: notificationGroup.localId, + }; + }).concat(notifications); + } + // native notification request + if (props.filter === 'all' && this.env.messaging.isNotificationPermissionDefault()) { + notifications.unshift({ + type: 'odoobotRequest', + uniqueId: 'odoobotRequest', + }); + } + return { + isDeviceMobile: this.env.messaging.device.isMobile, + notifications, + }; + } + + /** + * @private + * @param {Object} props + * @throws {Error} in case `props.filter` is not supported + * @returns {mail.thread[]} + */ + _useStoreSelectorThreads(props) { + if (props.filter === 'mailbox') { + return this.env.models['mail.thread'] + .all(thread => thread.isPinned && thread.model === 'mail.box') + .sort((mailbox1, mailbox2) => { + if (mailbox1 === this.env.messaging.inbox) { + return -1; + } + if (mailbox2 === this.env.messaging.inbox) { + return 1; + } + if (mailbox1 === this.env.messaging.starred) { + return -1; + } + if (mailbox2 === this.env.messaging.starred) { + return 1; + } + const mailbox1Name = mailbox1.displayName; + const mailbox2Name = mailbox2.displayName; + mailbox1Name < mailbox2Name ? -1 : 1; + }); + } else if (props.filter === 'channel') { + return this.env.models['mail.thread'] + .all(thread => + thread.channel_type === 'channel' && + thread.isPinned && + thread.model === 'mail.channel' + ) + .sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1); + } else if (props.filter === 'chat') { + return this.env.models['mail.thread'] + .all(thread => + thread.isChatChannel && + thread.isPinned && + thread.model === 'mail.channel' + ) + .sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1); + } else if (props.filter === 'all') { + // "All" filter is for channels and chats + return this.env.models['mail.thread'] + .all(thread => thread.isPinned && thread.model === 'mail.channel') + .sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1); + } else { + throw new Error(`Unsupported filter ${props.filter}`); + } + } + +} + +Object.assign(NotificationList, { + _allowedFilters: ['all', 'mailbox', 'channel', 'chat'], + components, + defaultProps: { + filter: 'all', + }, + props: { + filter: { + type: String, + validate: prop => NotificationList._allowedFilters.includes(prop), + }, + }, + template: 'mail.NotificationList', +}); + +return NotificationList; + +}); diff --git a/addons/mail/static/src/components/notification_list/notification_list.scss b/addons/mail/static/src/components/notification_list/notification_list.scss new file mode 100644 index 00000000..18e31149 --- /dev/null +++ b/addons/mail/static/src/components/notification_list/notification_list.scss @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ + + .o_NotificationList { + display: flex; + flex-flow: column; + overflow: auto; + + &.o-empty { + justify-content: center; + } +} + +.o_NotificationList_noConversation { + display: flex; + align-items: center; + justify-content: center; + padding: map-get($spacers, 4) map-get($spacers, 2); +} + +.o_NotificationList_separator { + flex: 0 0 auto; + width: map-get($sizes, 100); +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + +.o_NotificationList_separator { + border-bottom: $border-width solid $border-color; +} + +.o_NotificationList_noConversation { + color: $text-muted; +} diff --git a/addons/mail/static/src/components/notification_list/notification_list.xml b/addons/mail/static/src/components/notification_list/notification_list.xml new file mode 100644 index 00000000..e3bfbf38 --- /dev/null +++ b/addons/mail/static/src/components/notification_list/notification_list.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="mail.NotificationList" owl="1"> + <div class="o_NotificationList" t-att-class="{ 'o-empty': notifications.length === 0 }"> + <t t-if="notifications.length === 0"> + <div class="o_NotificationList_noConversation"> + No conversation yet... + </div> + </t> + <t t-else=""> + <t t-foreach="notifications" t-as="notification" t-key="notification.uniqueId"> + <t t-if="notification.type === 'thread' and notification.thread"> + <ThreadPreview + class="o_NotificationList_preview" + t-att-class="{ 'o-mobile': env.messaging.device.isMobile }" + threadLocalId="notification.thread.localId" + /> + </t> + <t t-if="notification.type === 'thread_needaction' and notification.thread"> + <ThreadNeedactionPreview + class="o_NotificationList_preview" + t-att-class="{ 'o-mobile': env.messaging.device.isMobile }" + threadLocalId="notification.thread.localId" + /> + </t> + <t t-if="notification.notificationGroup"> + <NotificationGroup + class="o_NotificationList_group" + notificationGroupLocalId="notification.notificationGroup.localId" + /> + </t> + <t t-if="notification.type === 'odoobotRequest'"> + <NotificationRequest + class="o_NotificationList_notificationRequest" + t-att-class="{ 'o-mobile': env.messaging.device.isMobile }" + /> + </t> + <t t-if="!notification_last"> + <div class="o_NotificationList_separator"/> + </t> + </t> + </t> + </div> + </t> + +</templates> diff --git a/addons/mail/static/src/components/notification_list/notification_list_item.scss b/addons/mail/static/src/components/notification_list/notification_list_item.scss new file mode 100644 index 00000000..af98a9fb --- /dev/null +++ b/addons/mail/static/src/components/notification_list/notification_list_item.scss @@ -0,0 +1,179 @@ +// ----------------------------------------------------------------------------- +// Layout +// ----------------------------------------------------------------------------- + +@mixin o-mail-notification-list-item-layout { + display: flex; + flex: 0 0 auto; // Without this, Safari shrinks parent regardless of child content + align-items: center; + padding: map-get($spacers, 1); + + &.o-mobile { + padding: map-get($spacers, 2); + } +} + +@mixin o-mail-notification-list-item-content-layout { + display: flex; + flex-flow: column; + flex: 1 1 auto; + align-self: flex-start; + min-width: 0; // needed for flex to work correctly + margin: map-get($spacers, 2); +} + +@mixin o-mail-notification-list-item-core-layout { + display: flex; +} + +@mixin o-mail-notification-list-item-core-item-layout { + margin: map-get($spacers, 0) map-get($spacers, 2); + + &:first-child { + margin-inline-start: map-get($spacers, 0); + } + + &:last-child { + margin-inline-end: map-get($spacers, 0); + } +} + +@mixin o-mail-notification-list-item-counter-layout() { + margin: map-get($spacers, 0) map-get($spacers, 2); +} + +@mixin o-mail-notification-list-item-date-layout() { + flex: 0 0 auto; +} + +@mixin o-mail-notification-list-item-header-layout { + display: flex; + margin-bottom: map-get($spacers, 1); +} + +@mixin o-mail-notification-list-item-image-layout { + width: map-get($sizes, 100); + height: map-get($sizes, 100); +} + +@mixin o-mail-notification-list-item-image-container-layout { + position: relative; + width: 40px; + height: 40px; +} + +@mixin o-mail-notification-list-item-inline-text-layout { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &.o-empty::before { + content: '\00a0'; // keep line-height as if it had content + } +} + +@mixin o-mail-notification-list-item-mark-as-read-layout() { + display: flex; + flex: 0 0 auto; +} + +@mixin o-mail-notification-list-item-name-layout { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &.o-mobile { + font-size: 1.1em; + } +} + +@mixin o-mail-notification-list-item-partner-im-status-icon-layout { + @include o-position-absolute($bottom: 0, $right: 0); + display: flex; + align-items: center; + justify-content: center; +} + +@mixin o-mail-notification-list-item-sidebar-layout { + margin: map-get($spacers, 1); +} + +// ----------------------------------------------------------------------------- +// Style +// ----------------------------------------------------------------------------- + +$o-mail-notification-list-item-background-color: $white !default; +$o-mail-notification-list-item-hover-background-color: + darken($o-mail-notification-list-item-background-color, 7%) !default; + +$o-mail-notification-list-item-muted-background-color: gray('100') !default; +$o-mail-notification-list-item-muted-hover-background-color: + darken($o-mail-notification-list-item-muted-background-color, 7%) !default; + +@mixin o-mail-notification-list-item-style { + cursor: pointer; + user-select: none; + background-color: $o-mail-notification-list-item-background-color; + + &:hover { + background-color: $o-mail-notification-list-item-hover-background-color; + } + + &.o-muted { + background-color: $o-mail-notification-list-item-muted-background-color; + + &:hover { + background-color: $o-mail-notification-list-item-muted-hover-background-color; + } + } +} + +@mixin o-mail-notification-list-item-bold-style { + font-weight: bold; + + &.o-muted { + font-weight: initial; + } +} + +@mixin o-mail-notification-list-item-core-style { + color: gray('500'); +} + +@mixin o-mail-notification-list-item-date-style() { + @include o-mail-notification-list-item-bold-style(); + font-size: x-small; + color: $o-brand-primary; +} + +@mixin o-mail-notification-list-item-image-style { + object-fit: cover; +} + +@mixin o-mail-notification-list-item-mark-as-read-style() { + opacity: 0; + + &:hover { + color: gray('600'); + } +} + +@mixin o-mail-notification-list-item-hover-partner-im-status-icon-style { + color: $o-mail-notification-list-item-hover-background-color; +} + +@mixin o-mail-notification-list-item-muted-hover-partner-im-status-icon-style { + color: $o-mail-notification-list-item-muted-hover-background-color; +} + +@mixin o-mail-notification-list-item-partner-im-status-icon-style { + color: $o-mail-notification-list-item-background-color; + + &:not(.o-mobile) { + font-size: x-small; + } + + &.o-muted { + color: $o-mail-notification-list-item-muted-background-color; + } +} diff --git a/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js b/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js new file mode 100644 index 00000000..223ce363 --- /dev/null +++ b/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js @@ -0,0 +1,546 @@ +odoo.define('mail/static/src/components/notification_list/notification_list_notification_group_tests.js', function (require) { +'use strict'; + +const components = { + NotificationList: require('mail/static/src/components/notification_list/notification_list.js'), +}; + +const { + afterEach, + afterNextRender, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const Bus = require('web.Bus'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('notification_list', {}, function () { +QUnit.module('notification_list_notification_group_tests.js', { + beforeEach() { + beforeEach(this); + + /** + * @param {Object} param0 + * @param {string} [param0.filter='all'] + */ + this.createNotificationListComponent = async ({ filter = 'all' } = {}) => { + await createRootComponent(this, components.NotificationList, { + props: { filter }, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('notification group basic layout', async function (assert) { + assert.expect(10); + + // message that is expected to have a failure + this.data['mail.message'].records.push({ + id: 11, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'mail.channel', // expected value to link message to channel + res_id: 31, // id of a random channel + res_model_name: "Channel", // random res model name, will be asserted in the test + }); + // failure that is expected to be used in the test + this.data['mail.notification'].records.push({ + mail_message_id: 11, // id of the related message + notification_status: 'exception', // necessary value to have a failure + notification_type: 'email', // expected failure type for email message + }); + await this.start(); + await this.createNotificationListComponent(); + assert.containsOnce( + document.body, + '.o_NotificationGroup', + "should have 1 notification group" + ); + assert.containsOnce( + document.body, + '.o_NotificationGroup_name', + "should have 1 group name" + ); + assert.strictEqual( + document.querySelector('.o_NotificationGroup_name').textContent, + "Channel", + "should have model name as group name" + ); + assert.containsOnce( + document.body, + '.o_NotificationGroup_counter', + "should have 1 group counter" + ); + assert.strictEqual( + document.querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(1)", + "should have only 1 notification in the group" + ); + assert.containsOnce( + document.body, + '.o_NotificationGroup_date', + "should have 1 group date" + ); + assert.strictEqual( + document.querySelector('.o_NotificationGroup_date').textContent, + "a few seconds ago", + "should have the group date corresponding to now" + ); + assert.containsOnce( + document.body, + '.o_NotificationGroup_inlineText', + "should have 1 group text" + ); + assert.strictEqual( + document.querySelector('.o_NotificationGroup_inlineText').textContent.trim(), + "An error occurred when sending an email.", + "should have the group text corresponding to email" + ); + assert.containsOnce( + document.body, + '.o_NotificationGroup_markAsRead', + "should have 1 mark as read button" + ); +}); + +QUnit.test('mark as read', async function (assert) { + assert.expect(6); + + // message that is expected to have a failure + this.data['mail.message'].records.push({ + id: 11, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'mail.channel', // expected value to link message to channel + res_id: 31, // id of a random channel + res_model_name: "Channel", // random res model name, will be asserted in the test + }); + // failure that is expected to be used in the test + this.data['mail.notification'].records.push({ + mail_message_id: 11, // id of the related message + notification_status: 'exception', // necessary value to have a failure + notification_type: 'email', // expected failure type for email message + }); + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action, + 'mail.mail_resend_cancel_action', + "action should be the one to cancel email" + ); + assert.strictEqual( + payload.options.additional_context.default_model, + 'mail.channel', + "action should have the group model as default_model" + ); + assert.strictEqual( + payload.options.additional_context.unread_counter, + 1, + "action should have the group notification length as unread_counter" + ); + }); + await this.start({ env: { bus } }); + await this.createNotificationListComponent(); + assert.containsOnce( + document.body, + '.o_NotificationGroup_markAsRead', + "should have 1 mark as read button" + ); + + document.querySelector('.o_NotificationGroup_markAsRead').click(); + assert.verifySteps( + ['do_action'], + "should do an action to display the cancel email dialog" + ); +}); + +QUnit.test('grouped notifications by document', async function (assert) { + // If some failures linked to a document refers to a same document, a single + // notification should group all those failures. + assert.expect(5); + + this.data['mail.message'].records.push( + // first message that is expected to have a failure + { + id: 11, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'res.partner', // same model as second message (and not `mail.channel`) + res_id: 31, // same res_id as second message + res_model_name: "Partner", // random related model name + }, + // second message that is expected to have a failure + { + id: 12, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'res.partner', // same model as first message (and not `mail.channel`) + res_id: 31, // same res_id as first message + res_model_name: "Partner", // same related model name for consistency + } + ); + this.data['mail.notification'].records.push( + // first failure that is expected to be used in the test + { + mail_message_id: 11, // id of the related first message + notification_status: 'exception', // one possible value to have a failure + notification_type: 'email', // expected failure type for email message + }, + // second failure that is expected to be used in the test + { + mail_message_id: 12, // id of the related second message + notification_status: 'bounce', // other possible value to have a failure + notification_type: 'email', // expected failure type for email message + } + ); + await this.start({ hasChatWindow: true }); + await this.createNotificationListComponent(); + + assert.containsOnce( + document.body, + '.o_NotificationGroup', + "should have 1 notification group" + ); + assert.containsOnce( + document.body, + '.o_NotificationGroup_counter', + "should have 1 group counter" + ); + assert.strictEqual( + document.querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(2)", + "should have 2 notifications in the group" + ); + assert.containsNone( + document.body, + '.o_ChatWindow', + "should have no chat window initially" + ); + + await afterNextRender(() => + document.querySelector('.o_NotificationGroup').click() + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "should have opened the thread in a chat window after clicking on it" + ); +}); + +QUnit.test('grouped notifications by document model', async function (assert) { + // If all failures linked to a document model refers to different documents, + // a single notification should group all failures that are linked to this + // document model. + assert.expect(12); + + this.data['mail.message'].records.push( + // first message that is expected to have a failure + { + id: 11, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'res.partner', // same model as second message (and not `mail.channel`) + res_id: 31, // different res_id from second message + res_model_name: "Partner", // random related model name + }, + // second message that is expected to have a failure + { + id: 12, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'res.partner', // same model as first message (and not `mail.channel`) + res_id: 32, // different res_id from first message + res_model_name: "Partner", // same related model name for consistency + } + ); + this.data['mail.notification'].records.push( + // first failure that is expected to be used in the test + { + mail_message_id: 11, // id of the related first message + notification_status: 'exception', // one possible value to have a failure + notification_type: 'email', // expected failure type for email message + }, + // second failure that is expected to be used in the test + { + mail_message_id: 12, // id of the related second message + notification_status: 'bounce', // other possible value to have a failure + notification_type: 'email', // expected failure type for email message + } + ); + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action.name, + "Mail Failures", + "action should have 'Mail Failures' as name", + ); + assert.strictEqual( + payload.action.type, + 'ir.actions.act_window', + "action should have the type act_window" + ); + assert.strictEqual( + payload.action.view_mode, + 'kanban,list,form', + "action should have 'kanban,list,form' as view_mode" + ); + assert.strictEqual( + JSON.stringify(payload.action.views), + JSON.stringify([[false, 'kanban'], [false, 'list'], [false, 'form']]), + "action should have correct views" + ); + assert.strictEqual( + payload.action.target, + 'current', + "action should have 'current' as target" + ); + assert.strictEqual( + payload.action.res_model, + 'res.partner', + "action should have the group model as res_model" + ); + assert.strictEqual( + JSON.stringify(payload.action.domain), + JSON.stringify([['message_has_error', '=', true]]), + "action should have 'message_has_error' as domain" + ); + }); + + await this.start({ env: { bus } }); + await this.createNotificationListComponent(); + + assert.containsOnce( + document.body, + '.o_NotificationGroup', + "should have 1 notification group" + ); + assert.containsOnce( + document.body, + '.o_NotificationGroup_counter', + "should have 1 group counter" + ); + assert.strictEqual( + document.querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(2)", + "should have 2 notifications in the group" + ); + + document.querySelector('.o_NotificationGroup').click(); + assert.verifySteps( + ['do_action'], + "should do an action to display the related records" + ); +}); + +QUnit.test('different mail.channel are not grouped', async function (assert) { + // `mail.channel` is a special case where notifications are not grouped when + // they are linked to different channels, even though the model is the same. + assert.expect(6); + + this.data['mail.channel'].records.push({ id: 31 }, { id: 32 }); + this.data['mail.message'].records.push( + // first message that is expected to have a failure + { + id: 11, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'mail.channel', // testing a channel is the goal of the test + res_id: 31, // different res_id from second message + res_model_name: "Channel", // random related model name + }, + // second message that is expected to have a failure + { + id: 12, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'mail.channel', // testing a channel is the goal of the test + res_id: 32, // different res_id from first message + res_model_name: "Channel", // same related model name for consistency + } + ); + this.data['mail.notification'].records.push( + // first failure that is expected to be used in the test + { + mail_message_id: 11, // id of the related first message + notification_status: 'exception', // one possible value to have a failure + notification_type: 'email', // expected failure type for email message + }, + // second failure that is expected to be used in the test + { + mail_message_id: 12, // id of the related second message + notification_status: 'bounce', // other possible value to have a failure + notification_type: 'email', // expected failure type for email message + } + ); + await this.start({ + hasChatWindow: true, // needed to assert thread.open + }); + await this.createNotificationListComponent(); + assert.containsN( + document.body, + '.o_NotificationGroup', + 2, + "should have 2 notifications group" + ); + const groups = document.querySelectorAll('.o_NotificationGroup'); + assert.containsOnce( + groups[0], + '.o_NotificationGroup_counter', + "should have 1 group counter in first group" + ); + assert.strictEqual( + groups[0].querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(1)", + "should have 1 notification in first group" + ); + assert.containsOnce( + groups[1], + '.o_NotificationGroup_counter', + "should have 1 group counter in second group" + ); + assert.strictEqual( + groups[1].querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(1)", + "should have 1 notification in second group" + ); + + await afterNextRender(() => groups[0].click()); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "should have opened the channel related to the first group in a chat window" + ); +}); + +QUnit.test('multiple grouped notifications by document model, sorted by date desc', async function (assert) { + assert.expect(9); + + this.data['mail.message'].records.push( + // first message that is expected to have a failure + { + date: moment.utc().format("YYYY-MM-DD HH:mm:ss"), // random date + id: 11, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'res.partner', // different model from second message + res_id: 31, // random unique id, useful to link failure to message + res_model_name: "Partner", // random related model name + }, + // second message that is expected to have a failure + { + // random date, later than first message + date: moment.utc().add(1, 'days').format("YYYY-MM-DD HH:mm:ss"), + id: 12, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'res.company', // different model from first message + res_id: 32, // random unique id, useful to link failure to message + res_model_name: "Company", // random related model name + } + ); + this.data['mail.notification'].records.push( + // first failure that is expected to be used in the test + { + mail_message_id: 11, // id of the related first message + notification_status: 'exception', // one possible value to have a failure + notification_type: 'email', // expected failure type for email message + }, + // second failure that is expected to be used in the test + { + mail_message_id: 12, // id of the related second message + notification_status: 'bounce', // other possible value to have a failure + notification_type: 'email', // expected failure type for email message + } + ); + await this.start(); + await this.createNotificationListComponent(); + assert.containsN( + document.body, + '.o_NotificationGroup', + 2, + "should have 2 notifications group" + ); + const groups = document.querySelectorAll('.o_NotificationGroup'); + assert.containsOnce( + groups[0], + '.o_NotificationGroup_name', + "should have 1 group name in first group" + ); + assert.strictEqual( + groups[0].querySelector('.o_NotificationGroup_name').textContent, + "Company", + "should have first model name as group name" + ); + assert.containsOnce( + groups[0], + '.o_NotificationGroup_counter', + "should have 1 group counter in first group" + ); + assert.strictEqual( + groups[0].querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(1)", + "should have 1 notification in first group" + ); + assert.containsOnce( + groups[1], + '.o_NotificationGroup_name', + "should have 1 group name in second group" + ); + assert.strictEqual( + groups[1].querySelector('.o_NotificationGroup_name').textContent, + "Partner", + "should have second model name as group name" + ); + assert.containsOnce( + groups[1], + '.o_NotificationGroup_counter', + "should have 1 group counter in second group" + ); + assert.strictEqual( + groups[1].querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(1)", + "should have 1 notification in second group" + ); +}); + +QUnit.test('non-failure notifications are ignored', async function (assert) { + assert.expect(1); + + this.data['mail.message'].records.push( + // message that is expected to have a notification + { + id: 11, // random unique id, will be used to link failure to message + message_type: 'email', // message must be email (goal of the test) + model: 'res.partner', // random model + res_id: 31, // random unique id, useful to link failure to message + } + ); + this.data['mail.notification'].records.push( + // notification that is expected to be used in the test + { + mail_message_id: 11, // id of the related first message + notification_status: 'ready', // non-failure status + notification_type: 'email', // expected notification type for email message + }, + ); + await this.start(); + await this.createNotificationListComponent(); + assert.containsNone( + document.body, + '.o_NotificationGroup', + "should have 0 notification group" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/notification_list/notification_list_tests.js b/addons/mail/static/src/components/notification_list/notification_list_tests.js new file mode 100644 index 00000000..24df5b22 --- /dev/null +++ b/addons/mail/static/src/components/notification_list/notification_list_tests.js @@ -0,0 +1,162 @@ +odoo.define('mail/static/src/components/notification_list/notification_list_tests.js', function (require) { +'use strict'; + +const components = { + NotificationList: require('mail/static/src/components/notification_list/notification_list.js'), +}; + +const { + afterEach, + afterNextRender, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('notification_list', {}, function () { +QUnit.module('notification_list_tests.js', { + beforeEach() { + beforeEach(this); + + /** + * @param {Object} param0 + * @param {string} [param0.filter='all'] + */ + this.createNotificationListComponent = async ({ filter = 'all' }) => { + await createRootComponent(this, components.NotificationList, { + props: { filter }, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('marked as read thread notifications are ordered by last message date', async function (assert) { + assert.expect(3); + + this.data['mail.channel'].records.push( + { id: 100, name: "Channel 2019" }, + { id: 200, name: "Channel 2020" } + ); + this.data['mail.message'].records.push( + { + channel_ids: [100], + date: "2019-01-01 00:00:00", + id: 42, + model: 'mail.channel', + res_id: 100, + }, + { + channel_ids: [200], + date: "2020-01-01 00:00:00", + id: 43, + model: 'mail.channel', + res_id: 200, + } + ); + await this.start(); + await this.createNotificationListComponent({ filter: 'all' }); + assert.containsN( + document.body, + '.o_ThreadPreview', + 2, + "there should be two thread previews" + ); + const threadPreviewElList = document.querySelectorAll('.o_ThreadPreview'); + assert.strictEqual( + threadPreviewElList[0].querySelector(':scope .o_ThreadPreview_name').textContent, + 'Channel 2020', + "First channel in the list should be the channel of 2020 (more recent last message)" + ); + assert.strictEqual( + threadPreviewElList[1].querySelector(':scope .o_ThreadPreview_name').textContent, + 'Channel 2019', + "Second channel in the list should be the channel of 2019 (least recent last message)" + ); +}); + +QUnit.test('thread notifications are re-ordered on receiving a new message', async function (assert) { + assert.expect(4); + + this.data['mail.channel'].records.push( + { id: 100, name: "Channel 2019" }, + { id: 200, name: "Channel 2020" } + ); + this.data['mail.message'].records.push( + { + channel_ids: [100], + date: "2019-01-01 00:00:00", + id: 42, + model: 'mail.channel', + res_id: 100, + }, + { + channel_ids: [200], + date: "2020-01-01 00:00:00", + id: 43, + model: 'mail.channel', + res_id: 200, + } + ); + await this.start(); + await this.createNotificationListComponent({ filter: 'all' }); + assert.containsN( + document.body, + '.o_ThreadPreview', + 2, + "there should be two thread previews" + ); + + await afterNextRender(() => { + const messageData = { + author_id: [7, "Demo User"], + body: "<p>New message !</p>", + channel_ids: [100], + date: "2020-03-23 10:00:00", + id: 44, + message_type: 'comment', + model: 'mail.channel', + record_name: 'Channel 2019', + res_id: 100, + }; + this.widget.call('bus_service', 'trigger', 'notification', [ + [['my-db', 'mail.channel', 100], messageData] + ]); + }); + assert.containsN( + document.body, + '.o_ThreadPreview', + 2, + "there should still be two thread previews" + ); + const threadPreviewElList = document.querySelectorAll('.o_ThreadPreview'); + assert.strictEqual( + threadPreviewElList[0].querySelector(':scope .o_ThreadPreview_name').textContent, + 'Channel 2019', + "First channel in the list should now be 'Channel 2019'" + ); + assert.strictEqual( + threadPreviewElList[1].querySelector(':scope .o_ThreadPreview_name').textContent, + 'Channel 2020', + "Second channel in the list should now be 'Channel 2020'" + ); +}); + +}); +}); +}); + +}); |
