[IMP] mail, *: add view model for notification list items

* = im_livechat, sms, snailmail

Part of task-2728107

closes odoo/odoo#82239

X-original-commit: fadb57564209e1b9c4ee6ee993b117b288ac614d
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Sébastien Theys
2022-01-05 12:50:16 +00:00
parent 76efa26896
commit 671e5b885e
37 changed files with 591 additions and 456 deletions
@@ -1,32 +0,0 @@
/** @odoo-module **/
import { NotificationList } from '@mail/components/notification_list/notification_list';
import { patch } from 'web.utils';
const components = { NotificationList };
components.NotificationList._allowedFilters.push('livechat');
patch(components.NotificationList.prototype, 'im_livechat/static/src/components/notification_list/notification_list.js', {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Override to include livechat channels.
*
* @override
*/
_getThreads(props) {
if (props.filter === 'livechat') {
return this.messaging.models['Thread'].all(thread =>
thread.channel_type === 'livechat' &&
thread.isPinned &&
thread.model === 'mail.channel'
);
}
return this._super(...arguments);
},
});
@@ -16,7 +16,7 @@ patch(components.ThreadNeedactionPreview.prototype, 'thread_needaction_preview',
* @override
*/
image(...args) {
if (this.thread.channel_type === 'livechat') {
if (this.threadNeedactionPreviewView.thread.channel_type === 'livechat') {
return '/mail/static/src/img/smiley/avatar.jpg';
}
return this._super(...args);
@@ -16,7 +16,7 @@ patch(components.ThreadPreview.prototype, 'im_livechat/static/src/components/thr
* @override
*/
image(...args) {
if (this.thread.channel_type === 'livechat') {
if (this.threadPreviewView.thread.channel_type === 'livechat') {
return '/mail/static/src/img/smiley/avatar.jpg';
}
return this._super(...args);
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { patchRecordMethods } from '@mail/model/model_core';
import { replace } from '@mail/model/model_field_command';
import '@mail/models/notification_list_view/notification_list_view'; // ensure the model definition is loaded before the patch
patchRecordMethods('NotificationListView', {
/**
* @override
*/
_computeFilteredThreads() {
if (this.filter === 'livechat') {
return replace(this.messaging.models['Thread'].all(thread =>
thread.channel_type === 'livechat' &&
thread.isPinned &&
thread.model === 'mail.channel'
));
}
return this._super();
},
});
@@ -71,10 +71,9 @@
New Channel
</button>
</t>
<NotificationList
className="'o_Discuss_notificationList'"
filter="discuss.activeMobileNavbarTabId"
/>
</t>
<t t-if="discuss.notificationListView">
<NotificationList className="'o_Discuss_notificationList'" localId="discuss.notificationListView.localId"/>
</t>
<t t-if="discuss.mobileMessagingNavbarView">
<MobileMessagingNavbar
@@ -49,11 +49,9 @@
/>
</t>
</div>
<NotificationList
className="'o_MessagingMenu_notificationList'"
classNameObj="{ 'o-mobile': messaging.device.isMobile }"
filter="messagingMenu.activeTabId"
/>
<t t-if="messagingMenu.notificationListView">
<NotificationList className="'o_MessagingMenu_notificationList'" classNameObj="{ 'o-mobile': messaging.device.isMobile }" localId="messagingMenu.notificationListView.localId"/>
</t>
<t t-if="messagingMenu.mobileMessagingNavbarView">
<MobileMessagingNavbar
className="'o_MessagingMenu_mobileNavbar'"
@@ -24,17 +24,17 @@ export class NotificationGroup extends Component {
//--------------------------------------------------------------------------
/**
* @returns {NotificationGroup}
* @returns {NotificationGroupView}
*/
get group() {
return this.messaging && this.messaging.models['NotificationGroup'].get(this.props.notificationGroupLocalId);
get notificationGroupView() {
return this.messaging && this.messaging.models['NotificationGroupView'].get(this.props.localId);
}
/**
* @returns {string|undefined}
*/
image() {
if (this.group.notification_type === 'email') {
if (this.notificationGroupView.notificationGroup.notification_type === 'email') {
return '/mail/static/src/img/smiley/mailfailure.jpg';
}
}
@@ -53,7 +53,7 @@ export class NotificationGroup extends Component {
// handled in `_onClickMarkAsRead`
return;
}
this.group.openDocuments();
this.notificationGroupView.notificationGroup.openDocuments();
if (!this.messaging.device.isMobile) {
this.messaging.messagingMenu.close();
}
@@ -64,7 +64,7 @@ export class NotificationGroup extends Component {
* @param {MouseEvent} ev
*/
_onClickMarkAsRead(ev) {
this.group.openCancelAction();
this.notificationGroupView.notificationGroup.openCancelAction();
if (!this.messaging.device.isMobile) {
this.messaging.messagingMenu.close();
}
@@ -74,7 +74,7 @@ export class NotificationGroup extends Component {
Object.assign(NotificationGroup, {
props: {
notificationGroupLocalId: String,
localId: String,
},
template: 'mail.NotificationGroup',
});
@@ -3,7 +3,7 @@
<t t-name="mail.NotificationGroup" owl="1">
<div class="o_NotificationGroup" t-attf-class="{{ className }}" t-on-click="_onClick" t-ref="root">
<t t-if="group">
<t t-if="notificationGroupView">
<div class="o_NotificationGroup_sidebar">
<div class="o_NotificationGroup_imageContainer o_NotificationGroup_sidebarItem">
<img class="o_NotificationGroup_image rounded-circle" t-att-src="image()" alt="Message delivery failure image"/>
@@ -12,21 +12,21 @@
<div class="o_NotificationGroup_content">
<div class="o_NotificationGroup_header">
<span class="o_NotificationGroup_name text-truncate">
<t t-esc="group.res_model_name"/>
<t t-esc="notificationGroupView.notificationGroup.res_model_name"/>
</span>
<span class="o_NotificationGroup_counter">
(<t t-esc="group.notifications.length"/>)
(<t t-esc="notificationGroupView.notificationGroup.notifications.length"/>)
</span>
<span class="o-autogrow"/>
<t t-if="group.date">
<t t-if="notificationGroupView.notificationGroup.date">
<span class="o_NotificationGroup_date">
<t t-esc="group.date.fromNow()"/>
<t t-esc="notificationGroupView.notificationGroup.date.fromNow()"/>
</span>
</t>
</div>
<div class="o_NotificationGroup_core">
<span class="o_NotificationGroup_coreItem o_NotificationGroup_inlineText text-truncate">
<t t-if="group.notification_type === 'email'">
<t t-if="notificationGroupView.notificationGroup.notification_type === 'email'">
An error occurred when sending an email.
</t>
</span>
@@ -19,95 +19,11 @@ export class NotificationList extends Component {
this._loadPreviews();
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {Object[]}
* @returns {NotificationListView}
*/
get notifications() {
if (!this.messaging) {
return [];
}
const threads = this._getThreads(this.props);
let threadNeedactionNotifications = [];
if (this.props.filter === 'all') {
// threads with needactions
threadNeedactionNotifications = this.messaging.models['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.id < t2.lastNeedactionMessageAsOriginThread.id ? 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.id < t2.lastMessage.id ? 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 (this.props.filter === 'all') {
notifications = Object.values(this.messaging.models['NotificationGroup'].all())
.sort((group1, group2) => group1.sequence - group2.sequence)
.map(notificationGroup => {
return {
notificationGroup,
uniqueId: notificationGroup.localId,
};
}).concat(notifications);
}
// native notification request
if (this.props.filter === 'all' && this.messaging.isNotificationPermissionDefault) {
notifications.unshift({
type: 'odoobotRequest',
uniqueId: 'odoobotRequest',
});
}
return notifications;
get notificationListView() {
return this.messaging && this.messaging.models['NotificationListView'].get(this.props.localId);
}
//--------------------------------------------------------------------------
@@ -121,78 +37,15 @@ export class NotificationList extends Component {
* @private
*/
async _loadPreviews() {
const threads = this.notifications
.filter(notification => notification.thread && notification.thread.exists())
.map(notification => notification.thread);
const threads = this.notificationListView.threadPreviewViews
.map(threadPreviewView => threadPreviewView.thread);
this.messaging.models['Thread'].loadPreviews(threads);
}
/**
* @private
* @param {Object} props
* @throws {Error} in case `props.filter` is not supported
* @returns {Thread[]}
*/
_getThreads(props) {
if (props.filter === 'mailbox') {
return this.messaging.models['Thread']
.all(thread => thread.isPinned && thread.model === 'mail.box')
.sort((mailbox1, mailbox2) => {
if (mailbox1 === this.messaging.inbox) {
return -1;
}
if (mailbox2 === this.messaging.inbox) {
return 1;
}
if (mailbox1 === this.messaging.starred) {
return -1;
}
if (mailbox2 === this.messaging.starred) {
return 1;
}
const mailbox1Name = mailbox1.displayName;
const mailbox2Name = mailbox2.displayName;
mailbox1Name < mailbox2Name ? -1 : 1;
});
} else if (props.filter === 'channel') {
return this.messaging.models['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.messaging.models['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.messaging.models['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'],
defaultProps: {
filter: 'all',
},
props: {
filter: {
type: String,
validate: prop => NotificationList._allowedFilters.includes(prop),
},
},
props: { localId: String },
template: 'mail.NotificationList',
});
@@ -2,42 +2,45 @@
<templates xml:space="preserve">
<t t-name="mail.NotificationList" owl="1">
<div class="o_NotificationList" t-att-class="{ 'o-empty': notifications.length === 0 }" t-attf-class="{{ className }}" t-ref="root">
<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
className="'o_NotificationList_preview'"
classNameObj="{ 'o-mobile': messaging.device.isMobile }"
threadLocalId="notification.thread.localId"
/>
</t>
<t t-if="notification.type === 'thread_needaction' and notification.thread">
<ThreadNeedactionPreview
className="'o_NotificationList_preview'"
classNameObj="{ 'o-mobile': messaging.device.isMobile }"
threadLocalId="notification.thread.localId"
/>
</t>
<t t-if="notification.notificationGroup">
<NotificationGroup
className="'o_NotificationList_group'"
notificationGroupLocalId="notification.notificationGroup.localId"
/>
</t>
<t t-if="notification.type === 'odoobotRequest'">
<NotificationRequest
className="'o_NotificationList_notificationRequest'"
t-att-class="{ 'o-mobile': messaging.device.isMobile }"
/>
</t>
<t t-if="!notification_last">
<div class="o_NotificationList_separator"/>
<div class="o_NotificationList" t-att-class="{ 'o-empty': notificationListView and notificationListView.notificationViews.length === 0 }" t-attf-class="{{ className }}" t-ref="root">
<t t-if="notificationListView">
<t t-if="notificationListView.notificationViews.length === 0">
<div class="o_NotificationList_noConversation">
No conversation yet...
</div>
</t>
<t t-else="">
<t t-foreach="notificationListView.notificationViews" t-as="notificationView" t-key="notificationView.localId">
<t t-if="notificationView.constructor.name === 'ThreadPreviewView'">
<ThreadPreview
className="'o_NotificationList_preview'"
classNameObj="{ 'o-mobile': messaging.device.isMobile }"
localId="notificationView.localId"
/>
</t>
<t t-if="notificationView.constructor.name === 'ThreadNeedactionPreviewView'">
<ThreadNeedactionPreview
className="'o_NotificationList_preview'"
classNameObj="{ 'o-mobile': messaging.device.isMobile }"
localId="notificationView.localId"
/>
</t>
<t t-if="notificationView.constructor.name === 'NotificationGroupView'">
<NotificationGroup
className="'o_NotificationList_group'"
localId="notificationView.localId"
/>
</t>
<t t-if="notificationView.constructor.name === 'NotificationRequestView'">
<NotificationRequest
className="'o_NotificationList_notificationRequest'"
t-att-class="{ 'o-mobile': messaging.device.isMobile }"
localId="notificationView.localId"
/>
</t>
<t t-if="!notificationView_last">
<div class="o_NotificationList_separator"/>
</t>
</t>
</t>
</t>
@@ -1,12 +1,6 @@
/** @odoo-module **/
import {
afterEach,
afterNextRender,
beforeEach,
createRootMessagingComponent,
start,
} from '@mail/utils/test_utils';
import { afterEach, afterNextRender, beforeEach, start, } from '@mail/utils/test_utils';
import Bus from 'web.Bus';
@@ -17,23 +11,14 @@ 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 createRootMessagingComponent(this, "NotificationList", {
props: { filter },
target: this.widget.el,
});
};
this.start = async params => {
const { env, widget } = await start(Object.assign({}, params, {
const res = await start(Object.assign({}, params, {
data: this.data,
}));
const { env, widget } = res;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -58,8 +43,8 @@ QUnit.test('notification group basic layout', async function (assert) {
notification_status: 'exception', // necessary value to have a failure
notification_type: 'email', // expected failure type for email message
});
await this.start();
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start();
await createNotificationListComponent();
assert.containsOnce(
document.body,
'.o_NotificationGroup',
@@ -148,8 +133,8 @@ QUnit.test('mark as read', async function (assert) {
"action should have the group notification length as unread_counter"
);
});
await this.start({ env: { bus } });
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start({ env: { bus } });
await createNotificationListComponent();
assert.containsOnce(
document.body,
'.o_NotificationGroup_markAsRead',
@@ -200,8 +185,8 @@ QUnit.test('grouped notifications by document', async function (assert) {
notification_type: 'email', // expected failure type for email message
}
);
await this.start({ hasChatWindow: true });
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start({ hasChatWindow: true });
await createNotificationListComponent();
assert.containsOnce(
document.body,
@@ -312,8 +297,8 @@ QUnit.test('grouped notifications by document model', async function (assert) {
);
});
await this.start({ env: { bus } });
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start({ env: { bus } });
await createNotificationListComponent();
assert.containsOnce(
document.body,
@@ -376,10 +361,10 @@ QUnit.test('different mail.channel are not grouped', async function (assert) {
notification_type: 'email', // expected failure type for email message
}
);
await this.start({
const { createNotificationListComponent } = await this.start({
hasChatWindow: true, // needed to assert thread.open
});
await this.createNotificationListComponent();
await createNotificationListComponent();
assert.containsN(
document.body,
'.o_NotificationGroup',
@@ -451,8 +436,8 @@ QUnit.test('multiple grouped notifications by document model, sorted by the most
notification_type: 'email', // expected failure type for email message
}
);
await this.start();
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start();
await createNotificationListComponent();
assert.containsN(
document.body,
'.o_NotificationGroup',
@@ -522,8 +507,8 @@ QUnit.test('non-failure notifications are ignored', async function (assert) {
notification_type: 'email', // expected notification type for email message
},
);
await this.start();
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start();
await createNotificationListComponent();
assert.containsNone(
document.body,
'.o_NotificationGroup',
@@ -1,12 +1,6 @@
/** @odoo-module **/
import {
afterEach,
afterNextRender,
beforeEach,
createRootMessagingComponent,
start,
} from '@mail/utils/test_utils';
import { afterEach, afterNextRender, beforeEach, start } from '@mail/utils/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
@@ -15,23 +9,14 @@ QUnit.module('notification_list_tests.js', {
beforeEach() {
beforeEach(this);
/**
* @param {Object} param0
* @param {string} [param0.filter='all']
*/
this.createNotificationListComponent = async ({ filter = 'all' }) => {
await createRootMessagingComponent(this, "NotificationList", {
props: { filter },
target: this.widget.el,
});
};
this.start = async params => {
const { env, widget } = await start(Object.assign({}, params, {
const res = await start(Object.assign({}, params, {
data: this.data,
}));
const { env, widget } = res;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -60,8 +45,8 @@ QUnit.test('marked as read thread notifications are ordered by last message date
res_id: 200,
}
);
await this.start();
await this.createNotificationListComponent({ filter: 'all' });
const { createNotificationListComponent } = await this.start();
await createNotificationListComponent({ filter: 'all' });
assert.containsN(
document.body,
'.o_ThreadPreview',
@@ -102,8 +87,8 @@ QUnit.test('thread notifications are re-ordered on receiving a new message', asy
res_id: 200,
}
);
await this.start();
await this.createNotificationListComponent({ filter: 'all' });
const { createNotificationListComponent } = await this.start();
await createNotificationListComponent({ filter: 'all' });
assert.containsN(
document.body,
'.o_ThreadPreview',
@@ -62,7 +62,7 @@ export class NotificationRequest extends Component {
}
Object.assign(NotificationRequest, {
props: {},
props: { localId: String },
template: 'mail.NotificationRequest',
});
@@ -1,12 +1,6 @@
/** @odoo-module **/
import {
afterEach,
afterNextRender,
beforeEach,
createRootMessagingComponent,
start,
} from '@mail/utils/test_utils';
import { afterEach, afterNextRender, beforeEach, start } from '@mail/utils/test_utils';
import Bus from 'web.Bus';
@@ -17,13 +11,6 @@ QUnit.module('thread_needaction_preview_tests.js', {
beforeEach() {
beforeEach(this);
this.createThreadNeedactionPreviewComponent = async props => {
await createRootMessagingComponent(this, "ThreadNeedactionPreview", {
props,
target: this.widget.el
});
};
this.start = async params => {
const res = await start(Object.assign({}, params, {
data: this.data,
@@ -31,14 +31,14 @@ export class ThreadNeedactionPreview extends Component {
* @returns {string}
*/
image() {
if (this.thread.moduleIcon) {
return this.thread.moduleIcon;
if (this.threadNeedactionPreviewView.thread.moduleIcon) {
return this.threadNeedactionPreviewView.thread.moduleIcon;
}
if (this.thread.correspondent) {
return this.thread.correspondent.avatarUrl;
if (this.threadNeedactionPreviewView.thread.correspondent) {
return this.threadNeedactionPreviewView.thread.correspondent.avatarUrl;
}
if (this.thread.model === 'mail.channel') {
return `/web/image/mail.channel/${this.thread.id}/avatar_128?unique=${this.thread.avatarCacheKey}`;
if (this.threadNeedactionPreviewView.thread.model === 'mail.channel') {
return `/web/image/mail.channel/${this.threadNeedactionPreviewView.thread.id}/avatar_128?unique=${this.threadNeedactionPreviewView.thread.avatarCacheKey}`;
}
return '/mail/static/src/img/smiley/avatar.jpg';
}
@@ -49,17 +49,17 @@ export class ThreadNeedactionPreview extends Component {
* @returns {string}
*/
get inlineLastNeedactionMessageAsOriginThreadBody() {
if (!this.thread.lastNeedactionMessageAsOriginThread) {
if (!this.threadNeedactionPreviewView.thread.lastNeedactionMessageAsOriginThread) {
return '';
}
return mailUtils.htmlToTextContentInline(this.thread.lastNeedactionMessageAsOriginThread.prettyBody);
return mailUtils.htmlToTextContentInline(this.threadNeedactionPreviewView.thread.lastNeedactionMessageAsOriginThread.prettyBody);
}
/**
* @returns {Thread}
* @returns {ThreadNeedactionPreviewView}
*/
get thread() {
return this.messaging && this.messaging.models['Thread'].get(this.props.threadLocalId);
get threadNeedactionPreviewView() {
return this.messaging && this.messaging.models['ThreadNeedactionPreviewView'].get(this.props.localId);
}
//--------------------------------------------------------------------------
@@ -76,7 +76,7 @@ export class ThreadNeedactionPreview extends Component {
// handled in `_onClickMarkAsRead`
return;
}
this.thread.open();
this.threadNeedactionPreviewView.thread.open();
if (!this.messaging.device.isMobile) {
this.messaging.messagingMenu.close();
}
@@ -88,17 +88,15 @@ export class ThreadNeedactionPreview extends Component {
*/
_onClickMarkAsRead(ev) {
this.messaging.models['Message'].markAllAsRead([
['model', '=', this.thread.model],
['res_id', '=', this.thread.id],
['model', '=', this.threadNeedactionPreviewView.thread.model],
['res_id', '=', this.threadNeedactionPreviewView.thread.id],
]);
}
}
Object.assign(ThreadNeedactionPreview, {
props: {
threadLocalId: String,
},
props: { localId: String },
template: 'mail.ThreadNeedactionPreview',
});
@@ -6,18 +6,18 @@
The preview template is used by the discuss in mobile, and by the systray
menu in order to show preview of threads.
-->
<div class="o_ThreadNeedactionPreview" t-attf-class="{{ className }}" t-on-click="_onClick" t-att-data-thread-local-id="thread ? thread.localId : undefined" t-ref="root">
<t t-if="thread">
<div class="o_ThreadNeedactionPreview" t-attf-class="{{ className }}" t-on-click="_onClick" t-att-data-thread-local-id="threadNeedactionPreviewView ? threadNeedactionPreviewView.thread.localId : undefined" t-ref="root">
<t t-if="threadNeedactionPreviewView">
<div class="o_ThreadNeedactionPreview_sidebar">
<div class="o_ThreadNeedactionPreview_imageContainer o_ThreadNeedactionPreview_sidebarItem">
<img class="o_ThreadNeedactionPreview_image" t-att-src="image()" alt="Thread Image"/>
<t t-if="thread.correspondent and thread.correspondent.im_status">
<t t-if="threadNeedactionPreviewView.thread.correspondent and threadNeedactionPreviewView.thread.correspondent.im_status">
<PartnerImStatusIcon
className="'o_ThreadNeedactionPreview_partnerImStatusIcon'"
classNameObj="{
'o-mobile': messaging.device.isMobile,
}"
partnerLocalId="thread.correspondent.localId"
partnerLocalId="threadNeedactionPreviewView.thread.correspondent.localId"
/>
</t>
</div>
@@ -25,24 +25,24 @@
<div class="o_ThreadNeedactionPreview_content">
<div class="o_ThreadNeedactionPreview_header">
<span class="o_ThreadNeedactionPreview_name text-truncate" t-att-class="{ 'o-mobile': messaging.device.isMobile }">
<t t-esc="thread.displayName"/>
<t t-esc="threadNeedactionPreviewView.thread.displayName"/>
</span>
<span class="o_ThreadNeedactionPreview_counter">
(<t t-esc="thread.needactionMessagesAsOriginThread.length"/>)
(<t t-esc="threadNeedactionPreviewView.thread.needactionMessagesAsOriginThread.length"/>)
</span>
<span class="o-autogrow"/>
<t t-if="thread.lastNeedactionMessageAsOriginThread and thread.lastNeedactionMessageAsOriginThread.date">
<t t-if="threadNeedactionPreviewView.thread.lastNeedactionMessageAsOriginThread and threadNeedactionPreviewView.thread.lastNeedactionMessageAsOriginThread.date">
<span class="o_ThreadNeedactionPreview_date">
<t t-esc="thread.lastNeedactionMessageAsOriginThread.date.fromNow()"/>
<t t-esc="threadNeedactionPreviewView.thread.lastNeedactionMessageAsOriginThread.date.fromNow()"/>
</span>
</t>
</div>
<div class="o_ThreadNeedactionPreview_core">
<span class="o_ThreadNeedactionPreview_coreItem o_ThreadNeedactionPreview_inlineText text-truncate" t-att-class="{ 'o-empty': inlineLastNeedactionMessageAsOriginThreadBody.length === 0 }">
<t t-if="thread.lastNeedactionMessageAsOriginThread and thread.lastNeedactionMessageAsOriginThread.author">
<t t-if="threadNeedactionPreviewView.thread.lastNeedactionMessageAsOriginThread and threadNeedactionPreviewView.thread.lastNeedactionMessageAsOriginThread.author">
<MessageAuthorPrefix
messageLocalId="thread.lastNeedactionMessageAsOriginThread.localId"
threadLocalId="thread.localId"
messageLocalId="threadNeedactionPreviewView.thread.lastNeedactionMessageAsOriginThread.localId"
threadLocalId="threadNeedactionPreviewView.thread.localId"
/>
</t>
<t t-esc="inlineLastNeedactionMessageAsOriginThreadBody"/>
@@ -1,12 +1,6 @@
/** @odoo-module **/
import {
afterEach,
afterNextRender,
beforeEach,
createRootMessagingComponent,
start,
} from '@mail/utils/test_utils';
import { afterEach, afterNextRender, beforeEach, start } from '@mail/utils/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
@@ -15,19 +9,14 @@ QUnit.module('thread_preview_tests.js', {
beforeEach() {
beforeEach(this);
this.createThreadPreviewComponent = async props => {
await createRootMessagingComponent(this, "ThreadPreview", {
props,
target: this.widget.el,
});
};
this.start = async params => {
const { env, widget } = await start(Object.assign({}, params, {
const res = await start(Object.assign({}, params, {
data: this.data,
}));
const { env, widget } = res;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -40,6 +29,7 @@ QUnit.test('mark as read', async function (assert) {
this.data['mail.channel'].records.push({
id: 11,
message_unread_counter: 1,
seen_message_id: 99,
});
this.data['mail.message'].records.push({
id: 100,
@@ -47,7 +37,7 @@ QUnit.test('mark as read', async function (assert) {
res_id: 11,
});
await this.start({
const { click, createMessagingMenuComponent } = await this.start({
hasChatWindow: true,
async mockRPC(route, args) {
if (route.includes('set_last_seen_message')) {
@@ -56,11 +46,8 @@ QUnit.test('mark as read', async function (assert) {
return this._super(...arguments);
},
});
const thread = this.messaging.models['Thread'].findFromIdentifyingData({
id: 11,
model: 'mail.channel',
});
await this.createThreadPreviewComponent({ threadLocalId: thread.localId });
await createMessagingMenuComponent();
await click('.o_MessagingMenu_toggler');
assert.containsOnce(
document.body,
'.o_ThreadPreview_markAsRead',
@@ -31,10 +31,10 @@ export class ThreadPreview extends Component {
* @returns {string}
*/
image() {
if (this.thread.correspondent) {
return this.thread.correspondent.avatarUrl;
if (this.threadPreviewView.thread.correspondent) {
return this.threadPreviewView.thread.correspondent.avatarUrl;
}
return `/web/image/mail.channel/${this.thread.id}/avatar_128?unique=${this.thread.avatarCacheKey}`;
return `/web/image/mail.channel/${this.threadPreviewView.thread.id}/avatar_128?unique=${this.threadPreviewView.thread.avatarCacheKey}`;
}
/**
@@ -43,17 +43,17 @@ export class ThreadPreview extends Component {
* @returns {string}
*/
get inlineLastMessageBody() {
if (!this.thread.lastMessage) {
if (!this.threadPreviewView.thread.lastMessage) {
return '';
}
return mailUtils.htmlToTextContentInline(this.thread.lastMessage.prettyBody);
return mailUtils.htmlToTextContentInline(this.threadPreviewView.thread.lastMessage.prettyBody);
}
/**
* @returns {Thread}
* @returns {ThreadPreviewView}
*/
get thread() {
return this.messaging && this.messaging.models['Thread'].get(this.props.threadLocalId);
get threadPreviewView() {
return this.messaging && this.messaging.models['ThreadPreviewView'].get(this.props.localId);
}
//--------------------------------------------------------------------------
@@ -70,7 +70,7 @@ export class ThreadPreview extends Component {
// handled in `_onClickMarkAsRead`
return;
}
this.thread.open();
this.threadPreviewView.thread.open();
if (!this.messaging.device.isMobile) {
this.messaging.messagingMenu.close();
}
@@ -81,17 +81,15 @@ export class ThreadPreview extends Component {
* @param {MouseEvent} ev
*/
_onClickMarkAsRead(ev) {
if (this.thread.lastNonTransientMessage) {
this.thread.markAsSeen(this.thread.lastNonTransientMessage);
if (this.threadPreviewView.thread.lastNonTransientMessage) {
this.threadPreviewView.thread.markAsSeen(this.threadPreviewView.thread.lastNonTransientMessage);
}
}
}
Object.assign(ThreadPreview, {
props: {
threadLocalId: String,
},
props: { localId: String },
template: 'mail.ThreadPreview',
});
@@ -6,55 +6,55 @@
The preview template is used by the discuss in mobile, and by the systray
menu in order to show preview of threads.
-->
<div class="o_ThreadPreview" t-att-class="{ 'o-muted': thread and thread.localMessageUnreadCounter === 0 }" t-attf-class="{{ className }}" t-on-click="_onClick" t-att-data-thread-local-id="thread ? thread.localId : undefined" t-ref="root">
<t t-if="thread">
<div class="o_ThreadPreview" t-att-class="{ 'o-muted': threadPreviewView and threadPreviewView.thread.localMessageUnreadCounter === 0 }" t-attf-class="{{ className }}" t-on-click="_onClick" t-att-data-thread-local-id="threadPreviewView ? threadPreviewView.thread.localId : undefined" t-ref="root">
<t t-if="threadPreviewView">
<div class="o_ThreadPreview_sidebar">
<div class="o_ThreadPreview_imageContainer o_ThreadPreview_sidebarItem">
<img class="o_ThreadPreview_image rounded-circle" t-att-src="image()" alt="Thread Image"/>
<t t-if="thread.correspondent and thread.correspondent.im_status">
<t t-if="threadPreviewView.thread.correspondent and threadPreviewView.thread.correspondent.im_status">
<PartnerImStatusIcon
className="'o_ThreadPreview_partnerImStatusIcon'"
classNameObj="{
'o-mobile': messaging.device.isMobile,
'o-muted': thread.localMessageUnreadCounter === 0,
'o-muted': threadPreviewView.thread.localMessageUnreadCounter === 0,
}"
partnerLocalId="thread.correspondent.localId"
partnerLocalId="threadPreviewView.thread.correspondent.localId"
/>
</t>
</div>
</div>
<div class="o_ThreadPreview_content">
<div class="o_ThreadPreview_header align-items-baseline">
<span class="o_ThreadPreview_name text-truncate" t-att-class="{ 'o-mobile': messaging.device.isMobile, 'o-muted': thread.localMessageUnreadCounter === 0 }">
<t t-esc="thread.displayName"/>
<span class="o_ThreadPreview_name text-truncate" t-att-class="{ 'o-mobile': messaging.device.isMobile, 'o-muted': threadPreviewView.thread.localMessageUnreadCounter === 0 }">
<t t-esc="threadPreviewView.thread.displayName"/>
</span>
<t t-if="thread.localMessageUnreadCounter > 0">
<span class="o_ThreadPreview_counter" t-att-class="{ 'o-muted': thread.localMessageUnreadCounter === 0 }">
(<t t-esc="thread.localMessageUnreadCounter"/>)
<t t-if="threadPreviewView.thread.localMessageUnreadCounter > 0">
<span class="o_ThreadPreview_counter" t-att-class="{ 'o-muted': threadPreviewView.thread.localMessageUnreadCounter === 0 }">
(<t t-esc="threadPreviewView.thread.localMessageUnreadCounter"/>)
</span>
</t>
<t t-if="thread.rtcSessions.length > 0">
<span class="o_ThreadPreview_callIndicator fa fa-volume-up mx-2" t-att-class="{ 'o-isCalling': thread.rtc }"/>
<t t-if="threadPreviewView.thread.rtcSessions.length > 0">
<span class="o_ThreadPreview_callIndicator fa fa-volume-up mx-2" t-att-class="{ 'o-isCalling': threadPreviewView.thread.rtc }"/>
</t>
<span class="o-autogrow"/>
<t t-if="thread.lastMessage and thread.lastMessage.date">
<span class="o_ThreadPreview_date" t-att-class="{ 'o-muted': thread.localMessageUnreadCounter === 0 }">
<t t-esc="thread.lastMessage.date.fromNow()"/>
<t t-if="threadPreviewView.thread.lastMessage and threadPreviewView.thread.lastMessage.date">
<span class="o_ThreadPreview_date" t-att-class="{ 'o-muted': threadPreviewView.thread.localMessageUnreadCounter === 0 }">
<t t-esc="threadPreviewView.thread.lastMessage.date.fromNow()"/>
</span>
</t>
</div>
<div class="o_ThreadPreview_core">
<span class="o_ThreadPreview_coreItem o_ThreadPreview_inlineText text-truncate" t-att-class="{ 'o-empty': inlineLastMessageBody.length === 0 }">
<t t-if="thread.lastMessage and thread.lastMessage.author">
<t t-if="threadPreviewView.thread.lastMessage and threadPreviewView.thread.lastMessage.author">
<MessageAuthorPrefix
messageLocalId="thread.lastMessage.localId"
threadLocalId="thread.localId"
messageLocalId="threadPreviewView.thread.lastMessage.localId"
threadLocalId="threadPreviewView.thread.localId"
/>
</t>
<t t-esc="inlineLastMessageBody"/>
</span>
<span class="o-autogrow"/>
<t t-if="thread.localMessageUnreadCounter > 0">
<t t-if="threadPreviewView.thread.localMessageUnreadCounter > 0">
<span class="o_ThreadPreview_coreItem o_ThreadPreview_markAsRead fa fa-check" title="Mark as Read" t-on-click="_onClickMarkAsRead" t-ref="markAsRead"/>
</t>
</div>
@@ -299,6 +299,13 @@ registerModel({
}
return clear();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeNotificationListView() {
return (this.messaging.device.isMobile && this.activeMobileNavbarTabId !== 'mailbox') ? insertAndReplace() : clear();
},
/**
* Only pinned threads are allowed in discuss.
*
@@ -401,6 +408,11 @@ registerModel({
menu_id: attr({
default: null,
}),
notificationListView: one2one('NotificationListView', {
compute: '_computeNotificationListView',
inverse: 'discussOwner',
isCausal: true,
}),
/**
* The navbar view on the discuss app when in mobile and when not
* replying to a message from inbox.
@@ -51,7 +51,7 @@ registerModel({
},
/**
* @private
* @returns {MobileMessagingNavbarView|FieldCommand}
* @returns {FieldCommand}
*/
_computeMobileMessagingNavbarView() {
if (this.messaging.device && this.messaging.device.isMobile) {
@@ -59,6 +59,12 @@ registerModel({
}
return clear();
},
/**
* @returns {FieldCommand}
*/
_computeNotificationListView() {
return this.isOpen ? insertAndReplace() : clear();
},
},
fields: {
/**
@@ -88,6 +94,11 @@ registerModel({
isOpen: attr({
default: false,
}),
notificationListView: one2one('NotificationListView', {
compute: '_computeNotificationListView',
inverse: 'messagingMenuOwner',
isCausal: true,
}),
/**
* The navbar view on the messaging menu when in mobile.
*/
@@ -127,6 +127,10 @@ registerModel({
notifications: one2many('Notification', {
inverse: 'notificationGroup',
}),
notificationGroupViews: one2many('NotificationGroupView', {
inverse: 'notificationGroup',
isCausal: true,
}),
res_id: attr({
readonly: true,
}),
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { many2one } from '@mail/model/model_field';
registerModel({
name: 'NotificationGroupView',
identifyingFields: ['notificationListViewOwner', 'notificationGroup'],
fields: {
notificationGroup: many2one('NotificationGroup', {
inverse: 'notificationGroupViews',
readonly: true,
required: true,
}),
notificationListViewOwner: many2one('NotificationListView', {
inverse: 'notificationGroupViews',
readonly: true,
required: true,
}),
},
});
@@ -0,0 +1,227 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, many2many, one2many, one2one } from '@mail/model/model_field';
import { clear, insertAndReplace, replace } from '@mail/model/model_field_command';
registerModel({
name: 'NotificationListView',
identifyingFields: [['discussOwner', 'messagingMenuOwner']],
recordMethods: {
/**
* @private
* @returns {string|FieldCommand}
*/
_computeFilter() {
if (this.discussOwner) {
return this.discussOwner.activeMobileNavbarTabId;
}
if (this.messagingMenuOwner) {
return this.messagingMenuOwner.activeTabId;
}
return clear();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeFilteredThreads() {
switch (this.filter) {
case 'mailbox': {
return replace(this.messaging.models['Thread']
.all(thread => thread.isPinned && thread.model === 'mail.box')
.sort((mailbox1, mailbox2) => {
if (mailbox1 === this.messaging.inbox) {
return -1;
}
if (mailbox2 === this.messaging.inbox) {
return 1;
}
if (mailbox1 === this.messaging.starred) {
return -1;
}
if (mailbox2 === this.messaging.starred) {
return 1;
}
const mailbox1Name = mailbox1.displayName;
const mailbox2Name = mailbox2.displayName;
mailbox1Name < mailbox2Name ? -1 : 1;
})
);
}
case 'channel': {
return replace(this.messaging.models['Thread']
.all(thread =>
thread.channel_type === 'channel' &&
thread.isPinned &&
thread.model === 'mail.channel'
)
.sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1)
);
}
case 'chat': {
return replace(this.messaging.models['Thread']
.all(thread =>
thread.isChatChannel &&
thread.isPinned &&
thread.model === 'mail.channel'
)
.sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1)
);
}
case 'all': {
// "All" filter is for channels and chats
return replace(this.messaging.models['Thread']
.all(thread => thread.isPinned && thread.model === 'mail.channel')
.sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1)
);
}
}
return clear();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeNotificationGroupViews() {
if (this.filter !== 'all') {
return clear();
}
return insertAndReplace(
this.models['NotificationGroup']
.all()
.sort((group1, group2) => group1.sequence - group2.sequence)
.map(notificationGroup => {
return { notificationGroup: replace(notificationGroup) };
})
);
},
/**
* @private
* @returns {FieldCommand}
*/
_computeNotificationRequestView() {
return (this.filter === 'all' && this.messaging.isNotificationPermissionDefault) ? insertAndReplace() : clear();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeNotificationViews() {
const notifications = [];
if (this.notificationRequestView) {
notifications.push(this.notificationRequestView);
}
notifications.push(...this.notificationGroupViews);
notifications.push(...this.threadNeedactionPreviewViews);
notifications.push(...this.threadPreviewViews);
return replace(notifications);
},
/**
* @private
* @returns {FieldCommand}
*/
_computeThreadNeedactionPreviewViews() {
if (this.filter !== 'all') {
return clear();
}
return insertAndReplace(
this.messaging.models['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.id < t2.lastNeedactionMessageAsOriginThread.id ? 1 : -1;
}
if (t1.lastNeedactionMessageAsOriginThread) {
return -1;
}
if (t2.lastNeedactionMessageAsOriginThread) {
return 1;
}
return t1.id < t2.id ? -1 : 1;
})
.map(thread => {
return { thread: replace(thread) };
})
);
},
/**
* @private
* @returns {FieldCommand}
*/
_computeThreadPreviewViews() {
return insertAndReplace(
this.filteredThreads
.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.id < t2.lastMessage.id ? 1 : -1;
}
if (t1.lastMessage) {
return -1;
}
if (t2.lastMessage) {
return 1;
}
return t1.id < t2.id ? -1 : 1;
})
.map(thread => {
return {
thread: replace(thread),
};
})
);
},
},
fields: {
discussOwner: one2one('Discuss', {
inverse: 'notificationListView',
readonly: true,
}),
filter: attr({
compute: '_computeFilter',
}),
filteredThreads: many2many('Thread', {
compute: '_computeFilteredThreads',
}),
messagingMenuOwner: one2one('MessagingMenu', {
inverse: 'notificationListView',
readonly: true,
}),
notificationGroupViews: one2many('NotificationGroupView', {
compute: '_computeNotificationGroupViews',
inverse: 'notificationListViewOwner',
isCausal: true,
}),
notificationRequestView: one2one('NotificationRequestView', {
compute: '_computeNotificationRequestView',
inverse: 'notificationListViewOwner',
isCausal: true,
}),
notificationViews: one2many('Model', {
compute: '_computeNotificationViews',
isCausal: true,
}),
threadNeedactionPreviewViews: one2many('ThreadNeedactionPreviewView', {
compute: '_computeThreadNeedactionPreviewViews',
inverse: 'notificationListViewOwner',
isCausal: true,
}),
threadPreviewViews: one2many('ThreadPreviewView', {
compute: '_computeThreadPreviewViews',
inverse: 'notificationListViewOwner',
isCausal: true,
}),
},
});
@@ -0,0 +1,25 @@
/** @odoo-module **/
import { addFields, patchIdentifyingFields, patchRecordMethods } from '@mail/model/model_core';
import { one2one } from '@mail/model/model_field';
import '@mail/models/notification_list_view/notification_list_view'; // ensure the model definition is loaded before the patch
addFields('NotificationListView', {
qunitTestOwner: one2one('QUnitTest', {
inverse: 'notificationListView',
readonly: true,
}),
});
patchIdentifyingFields('NotificationListView', identifyingFields => {
identifyingFields[0].push('qunitTestOwner');
});
patchRecordMethods('NotificationListView', {
_computeFilter() {
if (this.qunitTestOwner) {
return this.filter;
}
return this._super();
},
});
@@ -0,0 +1,16 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one2one } from '@mail/model/model_field';
registerModel({
name: 'NotificationRequestView',
identifyingFields: ['notificationListViewOwner'],
fields: {
notificationListViewOwner: one2one('NotificationListView', {
inverse: 'notificationRequestView',
required: true,
readonly: true,
}),
},
});
@@ -2421,6 +2421,14 @@ registerModel({
todayActivities: one2many('Activity', {
compute: '_computeTodayActivities',
}),
threadNeedactionPreviewViews: one2many('ThreadNeedactionPreviewView', {
inverse: 'thread',
isCausal: true,
}),
threadPreviewViews: one2many('ThreadPreviewView', {
inverse: 'thread',
isCausal: true,
}),
/**
* Members that are currently typing something in the composer of this
* thread, including current partner.
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { many2one } from '@mail/model/model_field';
registerModel({
name: 'ThreadNeedactionPreviewView',
identifyingFields: ['notificationListViewOwner', 'thread'],
fields: {
notificationListViewOwner: many2one('NotificationListView', {
inverse: 'threadNeedactionPreviewViews',
readonly: true,
required: true,
}),
thread: many2one('Thread', {
inverse: 'threadNeedactionPreviewViews',
readonly: true,
required: true,
}),
},
});
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { many2one } from '@mail/model/model_field';
registerModel({
name: 'ThreadPreviewView',
identifyingFields: ['notificationListViewOwner', 'thread'],
fields: {
notificationListViewOwner: many2one('NotificationListView', {
inverse: 'threadPreviewViews',
readonly: true,
required: true,
}),
thread: many2one('Thread', {
inverse: 'threadPreviewViews',
readonly: true,
required: true,
}),
},
});
@@ -472,6 +472,19 @@ function getCreateMessagingMenuComponent({ components, env, widget }) {
};
}
function getCreateNotificationListComponent({ components, env, modelManager, widget }) {
return async function createNotificationListComponent({ filter = 'all' } = {}) {
const notificationListView = modelManager.messaging.models['NotificationListView'].create({
filter,
qunitTestOwner: insertAndReplace(),
});
await createRootMessagingComponent({ components, env }, "NotificationList", {
props: { localId: notificationListView.localId },
target: widget.el,
});
};
}
function getCreateThreadViewComponent({ afterEvent, components, env, widget }) {
return async function createThreadViewComponent(threadView, otherProps = {}, { isFixedSize = false, waitUntilMessagesLoaded = true } = {}) {
let target;
@@ -830,6 +843,7 @@ async function start(param0 = {}) {
createComposerSuggestionComponent: getCreateComposerSuggestionComponent({ components, env: testEnv, modelManager, widget }),
createMessageComponent: getCreateMessageComponent({ components, env: testEnv, modelManager, widget }),
createMessagingMenuComponent: getCreateMessagingMenuComponent({ components, env: testEnv, widget }),
createNotificationListComponent: getCreateNotificationListComponent({ components, env: testEnv, modelManager, widget }),
createThreadViewComponent: getCreateThreadViewComponent({ afterEvent, components, env: testEnv, widget }),
openDiscuss,
};
+4
View File
@@ -18,6 +18,10 @@ registerModel({
inverse: 'qunitTest',
isCausal: true,
}),
notificationListView: one2one('NotificationListView', {
inverse: 'qunitTestOwner',
isCausal: true,
}),
threadViewer: one2one('ThreadViewer', {
inverse: 'qunitTest',
isCausal: true,
@@ -14,7 +14,7 @@ patch(NotificationGroup.prototype, 'sms/static/src/components/notification_group
* @override
*/
image() {
if (this.group.notification_type === 'sms') {
if (this.notificationGroupView.notificationGroup.notification_type === 'sms') {
return '/sms/static/img/sms_failure.svg';
}
return this._super(...arguments);
@@ -3,7 +3,7 @@
<t t-inherit="mail.NotificationGroup" t-inherit-mode="extension">
<xpath expr="//*[hasclass('o_NotificationGroup_inlineText')]" position="inside">
<t t-if="group.notification_type === 'sms'">
<t t-if="notificationGroupView.notificationGroup.notification_type === 'sms'">
An error occurred when sending an SMS.
</t>
</xpath>
@@ -1,14 +1,8 @@
odoo.define('sms/static/src/components/notification_list/notification_list_notification_group_tests.js', function (require) {
'use strict';
/** @odoo-module **/
const {
afterEach,
beforeEach,
createRootMessagingComponent,
start,
} = require('@mail/utils/test_utils');
import { afterEach, beforeEach, start } from '@mail/utils/test_utils';
const Bus = require('web.Bus');
import Bus from 'web.Bus';
QUnit.module('sms', {}, function () {
QUnit.module('components', {}, function () {
@@ -17,23 +11,14 @@ 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 createRootMessagingComponent(this, "NotificationList", {
props: { filter },
target: this.widget.el,
});
};
this.start = async params => {
const { env, widget } = await start(Object.assign({}, params, {
const res = await start(Object.assign({}, params, {
data: this.data,
}));
const { env, widget } = res;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -81,8 +66,8 @@ QUnit.test('mark as read', async function (assert) {
);
});
await this.start({ env: { bus } });
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start({ env: { bus } });
await createNotificationListComponent();
assert.containsOnce(
document.body,
@@ -132,8 +117,8 @@ QUnit.test('notifications grouped by notification_type', async function (assert)
notification_type: 'email', // different type from first failure
}
);
await this.start();
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start();
await createNotificationListComponent();
assert.containsN(
document.body,
@@ -272,8 +257,8 @@ QUnit.test('grouped notifications by document model', async function (assert) {
);
});
await this.start({ env: { bus } });
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start({ env: { bus } });
await createNotificationListComponent();
assert.containsOnce(
document.body,
@@ -301,5 +286,3 @@ QUnit.test('grouped notifications by document model', async function (assert) {
});
});
});
});
@@ -14,7 +14,7 @@ patch(NotificationGroup.prototype, 'snailmail/static/src/components/notification
* @override
*/
image() {
if (this.group.notification_type === 'snail') {
if (this.notificationGroupView.notificationGroup.notification_type === 'snail') {
return '/snailmail/static/img/snailmail_failure.png';
}
return this._super(...arguments);
@@ -3,7 +3,7 @@
<t t-inherit="mail.NotificationGroup" t-inherit-mode="extension">
<xpath expr="//*[hasclass('o_NotificationGroup_inlineText')]" position="inside">
<t t-if="group.notification_type === 'snail'">
<t t-if="notificationGroupView.notificationGroup.notification_type === 'snail'">
An error occurred when sending a letter with Snailmail.
</t>
</xpath>
@@ -1,11 +1,6 @@
/** @odoo-module **/
import {
afterEach,
beforeEach,
createRootMessagingComponent,
start,
} from '@mail/utils/test_utils';
import { afterEach, beforeEach, start } from '@mail/utils/test_utils';
import Bus from 'web.Bus';
@@ -16,23 +11,14 @@ 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 createRootMessagingComponent(this, "NotificationList", {
props: { filter },
target: this.widget.el,
});
};
this.start = async params => {
const { env, widget } = await start(Object.assign({}, params, {
const res = await start(Object.assign({}, params, {
data: this.data,
}));
const { env, widget } = res;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -75,8 +61,8 @@ QUnit.test('mark as read', async function (assert) {
"action should have the group notification length as unread_counter"
);
});
await this.start({ env: { bus } });
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start({ env: { bus } });
await createNotificationListComponent();
assert.containsOnce(
document.body,
@@ -126,8 +112,8 @@ QUnit.test('notifications grouped by notification_type', async function (assert)
notification_type: 'email', // different type from first failure
}
);
await this.start();
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start();
await createNotificationListComponent();
assert.containsN(
document.body,
@@ -266,8 +252,8 @@ QUnit.test('grouped notifications by document model', async function (assert) {
);
});
await this.start({ env: { bus } });
await this.createNotificationListComponent();
const { createNotificationListComponent } = await this.start({ env: { bus } });
await createNotificationListComponent();
assert.containsOnce(
document.body,