[IMP] mail, *: introduce view models (step 4)

*: sms, snailmail, test_mail

This commit introduces models that define records being 1:1 map with components,
as a step to move further to having essentially all business code in models.

Having code in models is desirable to have very maintainable code, thanks to
robust and declarative code with an ORM-like architecture.

Task-2831082

closes odoo/odoo#90320

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-05-03 03:56:58 +02:00
parent b71fc2199e
commit 241fa6c2b3
52 changed files with 542 additions and 351 deletions
@@ -2,19 +2,6 @@
// Layout
// ------------------------------------------------------------------
.o_ChannelInvitationForm_selectablePartnerAvatarContainer {
width: 32px;
height: 32px;
}
.o_ChannelInvitationForm_selectablePartnerAvatar {
object-fit: cover;
}
.o_ChannelInvitationForm_selectablePartnerName {
min-width: 0;
}
.o_ChannelInvitationForm_selectedPartners {
max-height: 100px;
}
@@ -11,25 +11,7 @@
</div>
<div class="d-flex flex-column flex-grow-1 mx-0 py-2 overflow-auto">
<t t-foreach="channelInvitationForm.selectablePartners" t-as="selectablePartner" t-key="selectablePartner.localId">
<div class="o_ChannelInvitationForm_selectablePartner d-flex align-items-center px-3 py-1 btn-light" t-on-click="() => channelInvitationForm.onClickSelectablePartner(selectablePartner)" t-att-data-partner-id="selectablePartner.id">
<div class="o_ChannelInvitationForm_selectablePartnerAvatarContainer position-relative flex-shrink-0">
<img class="o_ChannelInvitationForm_selectablePartnerAvatar w-100 h-100 rounded-circle" t-att-src="selectablePartner.avatarUrl" alt="Avatar"/>
<t t-if="selectablePartner.im_status and selectablePartner.im_status !== 'im_partner'">
<PartnerImStatusIcon
className="'o_ChannelInvitationForm_selectablePartnerImStatusIcon position-absolute bottom-0 end-0 d-flex align-items-center justify-content-center text-white'"
classNameObj="{
'o_ChannelInvitationForm_selectablePartnerImStatusIcon-mobile': messaging.device.isSmall,
'small' : !messaging.device.isSmall,
}"
partnerLocalId="selectablePartner.localId"
/>
</t>
</div>
<span class="o_ChannelInvitationForm_selectablePartnerName flex-grow-1 mx-2 text-truncate">
<t t-esc="selectablePartner.nameOrDisplayName"/>
</span>
<input class="o_ChannelInvitationForm_selectablePartnerCheckbox flex-shrink-0" type="checkbox" t-att-checked="channelInvitationForm.selectedPartners.includes(selectablePartner) ? 'checked' : undefined" t-on-input="ev => channelInvitationForm.onInputPartnerCheckbox(selectablePartner, ev)" t-ref="selection-status"/>
</div>
<ChannelInvitationFormSelectablePartner channelInvitationFormLocalId="channelInvitationForm.localId" selectablePartnerLocalId="selectablePartner.localId"/>
</t>
<t t-if="channelInvitationForm.selectablePartners.length === 0">
<div class="mx-3">No user found that is not already a member of this channel.</div>
@@ -45,9 +27,7 @@
<h4>Selected users:</h4>
<div class="o_ChannelInvitationForm_selectedPartners overflow-auto">
<t t-foreach="channelInvitationForm.selectedPartners" t-as="selectedPartner" t-key="selectedPartner.localId">
<button class="btn btn-secondary" t-on-click="() => channelInvitationForm.onClickSelectedPartner(selectedPartner)">
<t t-esc="selectedPartner.nameOrDisplayName"/> <i class="fa fa-times"/>
</button>
<ChannelInvitationFormSelectedPartner channelInvitationFormLocalId="channelInvitationForm.localId" selectedPartnerLocalId="selectedPartner.localId"/>
</t>
</div>
</div>
@@ -0,0 +1,33 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ChannelInvitationFormSelectablePartner extends Component {
/**
* @returns {ChannelInvitationForm}
*/
get channelInvitationForm() {
return this.messaging && this.messaging.models['ChannelInvitationForm'].get(this.props.channelInvitationFormLocalId);
}
/**
* @returns {Partner}
*/
get selectablePartner() {
return this.messaging && this.messaging.models['Partner'].get(this.props.selectablePartnerLocalId);
}
}
Object.assign(ChannelInvitationFormSelectablePartner, {
props: {
channelInvitationFormLocalId: String,
selectablePartnerLocalId: String,
},
template: 'mail.ChannelInvitationFormSelectablePartner',
});
registerMessagingComponent(ChannelInvitationFormSelectablePartner);
@@ -0,0 +1,14 @@
.o_ChannelInvitationFormSelectablePartner_avatar {
object-fit: cover;
}
.o_ChannelInvitationFormSelectablePartner_avatarContainer {
width: 32px;
height: 32px;
}
.o_ChannelInvitationForm_selectablePartnerName {
min-width: 0;
}
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ChannelInvitationFormSelectablePartner" owl="1">
<div class="o_ChannelInvitationFormSelectablePartner d-flex align-items-center px-3 py-1 btn-light" t-on-click="() => channelInvitationForm.onClickSelectablePartner(selectablePartner)" t-att-data-partner-id="selectablePartner.id" t-attf-class="{{ className }}" t-ref="root">
<div class="o_ChannelInvitationFormSelectablePartner_avatarContainer position-relative flex-shrink-0">
<img class="o_ChannelInvitationFormSelectablePartner_avatar w-100 h-100 rounded-circle" t-att-src="selectablePartner.avatarUrl" alt="Avatar"/>
<t t-if="selectablePartner.im_status and selectablePartner.im_status !== 'im_partner'">
<PartnerImStatusIcon
className="'o_ChannelInvitationFormSelectablePartner_imStatusIcon position-absolute bottom-0 end-0 d-flex align-items-center justify-content-center text-white'"
classNameObj="{
'o_ChannelInvitationFormSelectablePartner_imStatusIcon-mobile': messaging.device.isSmall,
'small' : !messaging.device.isSmall,
}"
partnerLocalId="selectablePartner.localId"
/>
</t>
</div>
<span class="o_ChannelInvitationFormSelectablePartner_name flex-grow-1 mx-2 text-truncate">
<t t-esc="selectablePartner.nameOrDisplayName"/>
</span>
<input class="o_ChannelInvitationFormSelectablePartner_checkbox flex-shrink-0" type="checkbox" t-att-checked="channelInvitationForm.selectedPartners.includes(selectablePartner) ? 'checked' : undefined" t-on-input="ev => channelInvitationForm.onInputPartnerCheckbox(selectablePartner, ev)" t-ref="selection-status"/>
</div>
</t>
</templates>
@@ -0,0 +1,33 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ChannelInvitationFormSelectedPartner extends Component {
/**
* @returns {ChannelInvitationForm}
*/
get channelInvitationForm() {
return this.messaging && this.messaging.models['ChannelInvitationForm'].get(this.props.channelInvitationFormLocalId);
}
/**
* @returns {Partner}
*/
get selectedPartner() {
return this.messaging && this.messaging.models['Partner'].get(this.props.selectedPartnerLocalId);
}
}
Object.assign(ChannelInvitationFormSelectedPartner, {
props: {
channelInvitationFormLocalId: String,
selectedPartnerLocalId: String,
},
template: 'mail.ChannelInvitationFormSelectedPartner',
});
registerMessagingComponent(ChannelInvitationFormSelectedPartner);
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ChannelInvitationFormSelectedPartner" owl="1">
<button class="btn btn-secondary" t-on-click="() => channelInvitationForm.onClickSelectedPartner(selectedPartner)">
<t t-esc="selectedPartner.nameOrDisplayName"/> <i class="fa fa-times"/>
</button>
</t>
</templates>
@@ -6,10 +6,6 @@
z-index: $zindex-dropdown;
}
.o_ChatWindowHiddenMenu_chatWindowHeader {
max-width: 200px;
}
.o_ChatWindowHiddenMenu_unreadCounter {
transform: translate(50%, -50%);
z-index: $zindex-tooltip; // on top of bootstrap dropup menu
@@ -19,10 +15,6 @@
// Style
// ------------------------------------------------------------------
.o_ChatWindowHiddenMenu_chatWindowHeader {
opacity: 0.95;
}
.o_ChatWindowHiddenMenu_windowCounter {
user-select: none;
}
@@ -11,14 +11,7 @@
</div>
<ul class="o_ChatWindowHiddenMenu_list dropdown-menu dropdown-menu-right m-0 p-0 overflow-auto" t-att-class="{ show: messaging.chatWindowManager.isHiddenMenuOpen }" role="menu" t-ref="list">
<t t-foreach="messaging.chatWindowManager.hiddenChatWindowHeaderViews" t-as="chatWindowHeaderView" t-key="chatWindowHeaderView.localId">
<li class="o_ChatWindowHiddenMenu_listItem" t-att-class="{ 'border-bottom' : !chatWindowHeaderView_last }" role="menuitem">
<ChatWindowHeader
className="'o_ChatWindowHiddenMenu_chatWindowHeader opacity-100-hover'"
chatWindowLocalId="chatWindowHeaderView.chatWindowOwner.localId"
localId="chatWindowHeaderView.localId"
onClicked="chatWindowHeaderView.chatWindowOwner.onClickFromChatWindowHiddenMenu"
/>
</li>
<ChatWindowHiddenMenuItem chatWindowHeaderViewLocalId="chatWindowHeaderView.localId" isLast="chatWindowHeaderView_last"/>
</t>
</ul>
<t t-if="messaging.chatWindowManager.unreadHiddenConversationAmount > 0">
@@ -0,0 +1,31 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ChatWindowHiddenMenuItem extends Component {
/**
* @returns {ChatWindowHeaderView}
*/
get chatWindowHeaderView() {
return this.messaging && this.messaging.models['ChatWindowHeaderView'].get(this.props.chatWindowHeaderViewLocalId);
}
}
Object.assign(ChatWindowHiddenMenuItem, {
defaultProps: {
isLast: false,
},
props: {
chatWindowHeaderViewLocalId: String,
isLast: {
type: Boolean,
optional: true,
},
},
template: 'mail.ChatWindowHiddenMenuItem',
});
registerMessagingComponent(ChatWindowHiddenMenuItem);
@@ -0,0 +1,15 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_ChatWindowHiddenMenu_chatWindowHeader {
max-width: 200px;
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_ChatWindowHiddenMenu_chatWindowHeader {
opacity: 0.95;
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ChatWindowHiddenMenuItem" owl="1">
<li class="o_ChatWindowHiddenMenuItem" t-att-class="{ 'border-bottom' : !props.isLast }" role="menuitem" t-attf-class="{{ className }}" t-ref="root">
<ChatWindowHeader
className="'o_ChatWindowHiddenMenuItem_chatWindowHeader opacity-100-hover'"
localId="chatWindowHeaderView.localId"
chatWindowLocalId="chatWindowHeaderView.chatWindowOwner.localId"
onClicked="chatWindowHeaderView.chatWindowOwner.onClickFromChatWindowHiddenMenu"
/>
</li>
</t>
</templates>
@@ -0,0 +1,26 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class Emoji extends Component {
/**
* @returns {EmojiListView}
*/
get emojiListView() {
return this.messaging && this.messaging.models['EmojiListView'].get(this.props.emojiListViewLocalId);
}
}
Object.assign(Emoji, {
props: {
emoji: Object,
emojiListViewLocalId: String,
},
template: 'mail.Emoji',
});
registerMessagingComponent(Emoji);
@@ -0,0 +1,16 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_Emoji {
font-size: 1.1em;
margin: map-get($spacers, 1);
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_Emoji {
cursor: pointer;
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.Emoji" owl="1">
<span class="o_Emoji" t-on-click="emojiListView.onClickEmoji" t-att-title="props.emoji.description" t-att-data-source="props.emoji.sources[0]" t-att-data-unicode="props.emoji.unicode" t-attf-class="{{ className }}" t-ref="root">
<t t-esc="props.emoji.unicode"/>
</span>
</t>
</templates>
@@ -8,15 +8,3 @@
max-width: 200px;
}
.o_EmojiList_emoji {
font-size: 1.1em;
margin: map-get($spacers, 1);
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_EmojiList_emoji {
cursor: pointer;
}
@@ -5,9 +5,7 @@
<div class="o_EmojiList" t-attf-class="{{ className }}" t-ref="root">
<t t-if="emojiListView">
<t t-foreach="emojis" t-as="emoji" t-key="emoji.unicode">
<span class="o_EmojiList_emoji" t-on-click="ev => emojiListView.onClickEmoji(ev)" t-att-title="emoji.description" t-att-data-source="emoji.sources[0]" t-att-data-unicode="emoji.unicode">
<t t-esc="emoji.unicode"/>
</span>
<Emoji emoji="emoji" emojiListViewLocalId="emojiListView.localId"/>
</t>
</t>
</div>
@@ -95,12 +95,6 @@
max-width: $o-mail-message-sidebar-width;
}
.o_Message_trackingValueItem {
// Could be replaced by "me-1" after the migration to BS5
// for the backend AND the frontend
margin-inline-end: map-get($spacers, 1);
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
@@ -25,7 +25,7 @@
}" t-attf-class="{{ messageView.extraClass }} {{ className }}" t-on-click="messageView.onClick" t-on-mouseenter="messageView.onMouseenter" t-on-mouseleave="messageView.onMouseleave" t-att-data-message-local-id="messageView.message.localId"
t-ref="root"
>
<MessageInReplyToView t-if="messageView.messageInReplyToView" messageInReplyToViewLocalId="messageView.messageInReplyToView.localId"/>
<MessageInReplyToView t-if="messageView.messageInReplyToView" localId="messageView.messageInReplyToView.localId"/>
<div t-if="messageView.isActive and messageView.messageActionList" class="o_Message_actionListContainer position-absolute pl-5 pr-3" t-att-class="{ 'o-squashed': messageView.isSquashed }">
<MessageActionList localId="messageView.messageActionList.localId"/>
</div>
@@ -122,9 +122,7 @@
<i name="notificationIcon" class="o_Message_notificationIcon fa fa-envelope-o"/>
</span>
<t t-set-slot="opened">
<NotificationPopover
messageViewLocalId="messageView.localId"
/>
<MessageNotificationPopoverContent localId="messageView.localId"/>
</t>
</Popover>
</t>
@@ -150,20 +148,7 @@
<t t-if="messageView.message.trackingValues.length > 0">
<ul class="o_Message_trackingValues mt-2">
<t t-foreach="messageView.message.trackingValues" t-as="value" t-key="value.id">
<li>
<div class="o_Message_trackingValue d-flex align-items-center flex-wrap">
<div class="o_Message_trackingValueFieldName o_Message_trackingValueItem" t-esc="value.changed_field"/>
<t t-if="value.old_value">
<div class="o_Message_trackingValueOldValue o_Message_trackingValueItem" t-esc="value.old_value"/>
</t>
<i t-else="" class="mr-2">None</i>
<div class="o_Message_trackingValueSeparator o_Message_trackingValueItem fa fa-long-arrow-right" title="Changed" role="img"/>
<t t-if="value.new_value">
<div class="o_Message_trackingValueNewValue o_Message_trackingValueItem" t-esc="value.new_value"/>
</t>
<i t-else="">None</i>
</div>
</li>
<TrackingValue value="value"/>
</t>
</ul>
</t>
@@ -10,12 +10,12 @@ export class MessageInReplyToView extends Component {
* @returns {MessageInReplyToView}
*/
get messageInReplyToView() {
return this.messaging && this.messaging.models['MessageInReplyToView'].get(this.props.messageInReplyToViewLocalId);
return this.messaging && this.messaging.models['MessageInReplyToView'].get(this.props.localId);
}
}
Object.assign(MessageInReplyToView, {
props: { messageInReplyToViewLocalId: String },
props: { localId: String },
template: "mail.MessageInReplyToView",
});
@@ -0,0 +1,23 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class MessageNotificationPopoverContent extends Component {
/**
* @returns {MessageView}
*/
get messageView() {
return this.messaging && this.messaging.models['MessageView'].get(this.props.localId);
}
}
Object.assign(MessageNotificationPopoverContent, {
props: { localId: String },
template: 'mail.MessageNotificationPopoverContent',
});
registerMessagingComponent(MessageNotificationPopoverContent);
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.MessageNotificationPopoverContent" owl="1">
<t t-if="messageView">
<div class="o_MessageNotificationPopoverContent" t-attf-class="{{ className }}" t-ref="root">
<t t-if="messageView.message">
<t t-foreach="messageView.message.notifications" t-as="notification" t-key="notification.localId">
<div class="o_MessageNotificationPopoverContent_notification">
<i class="o_MessageNotificationPopoverContent_notificationIcon me-2" t-att-class="notification.iconClass" t-att-title="notification.iconTitle" role="img"/>
<t t-if="notification.partner">
<span class="o_MessageNotificationPopoverContent_notificationPartnerName" t-esc="notification.partner.nameOrDisplayName"/>
</t>
</div>
</t>
</t>
</div>
</t>
</t>
</templates>
@@ -3,7 +3,7 @@
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component, onMounted, onWillUnmount } = owl;
const { Component } = owl;
export class MessagingMenu extends Component {
@@ -19,20 +19,6 @@ export class MessagingMenu extends Component {
* item is not considered as a click away from messaging menu in mobile.
*/
this.id = _.uniqueId('o_messagingMenu_');
// bind since passed as props
this._onMobileNewMessageInputSelect = this._onMobileNewMessageInputSelect.bind(this);
this._onMobileNewMessageInputSource = this._onMobileNewMessageInputSource.bind(this);
this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this);
onMounted(() => this._mounted());
onWillUnmount(() => this._willUnmount());
}
_mounted() {
document.addEventListener('click', this._onClickCaptureGlobal, true);
}
_willUnmount() {
document.removeEventListener('click', this._onClickCaptureGlobal, true);
}
//--------------------------------------------------------------------------
@@ -46,63 +32,6 @@ export class MessagingMenu extends Component {
return this.messaging && this.messaging.models['MessagingMenu'].get(this.props.localId);
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Closes the menu when clicking outside, if appropriate.
*
* @private
* @param {MouseEvent} ev
*/
_onClickCaptureGlobal(ev) {
if (!this.messagingMenu) {
return;
}
// ignore click inside the menu
if (!this.root.el || this.root.el.contains(ev.target)) {
return;
}
// in all other cases: close the messaging menu when clicking outside
this.messagingMenu.close();
}
/**
* @private
* @param {Event} ev
* @param {Object} ui
* @param {Object} ui.item
* @param {integer} ui.item.id
*/
_onMobileNewMessageInputSelect(ev, ui) {
this.messaging.openChat({ partnerId: ui.item.id });
}
/**
* @private
* @param {Object} req
* @param {string} req.term
* @param {function} res
*/
_onMobileNewMessageInputSource(req, res) {
const value = _.escape(req.term);
this.messaging.models['Partner'].imSearch({
callback: partners => {
const suggestions = partners.map(partner => {
return {
id: partner.id,
value: partner.nameOrDisplayName,
label: partner.nameOrDisplayName,
};
});
res(_.sortBy(suggestions, 'label'));
},
keyword: value,
limit: 10,
});
}
}
Object.assign(MessagingMenu, {
@@ -44,8 +44,8 @@
customClass="id + '_mobileNewMessageInputAutocomplete'"
isFocusOnMount="true"
localId="messagingMenu.mobileNewMessageAutocompleteInputView.localId"
select="_onMobileNewMessageInputSelect"
source="_onMobileNewMessageInputSource"
select="messagingMenu.onMobileNewMessageInputSelect"
source="messagingMenu.onMobileNewMessageInputSource"
onHide="messagingMenu.onHideMobileNewMessage"
/>
</t>
@@ -26,15 +26,6 @@ export class NotificationGroup extends Component {
return this.messaging && this.messaging.models['NotificationGroupView'].get(this.props.localId);
}
/**
* @returns {string|undefined}
*/
image() {
if (this.notificationGroupView.notificationGroup.notification_type === 'email') {
return '/mail/static/src/img/smiley/mailfailure.jpg';
}
}
}
Object.assign(NotificationGroup, {
@@ -6,7 +6,7 @@
<div class="o_NotificationGroup" t-attf-class="{{ className }}" t-on-click="notificationGroupView.onClick" t-ref="root">
<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"/>
<img class="o_NotificationGroup_image rounded-circle" t-att-src="notificationGroupView.imageSrc" alt="Message delivery failure image"/>
</div>
</div>
<div class="o_NotificationGroup_content">
@@ -2,22 +2,10 @@
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component, onMounted } = owl;
const { Component } = owl;
export class NotificationList extends Component {
/**
* @override
*/
setup() {
super.setup();
onMounted(() => this._mounted());
}
_mounted() {
this._loadPreviews();
}
/**
* @returns {NotificationListView}
*/
@@ -25,22 +13,6 @@ export class NotificationList extends Component {
return this.messaging && this.messaging.models['NotificationListView'].get(this.props.localId);
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Load previews of given thread. Basically consists of fetching all missing
* last messages of each thread.
*
* @private
*/
async _loadPreviews() {
const threads = this.notificationListView.threadPreviewViews
.map(threadPreviewView => threadPreviewView.thread);
this.messaging.models['Thread'].loadPreviews(threads);
}
}
Object.assign(NotificationList, {
@@ -1,23 +0,0 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class NotificationPopover extends Component {
/**
* @returns {MessageView}
*/
get messageView() {
return this.messaging && this.messaging.models['MessageView'].get(this.props.messageViewLocalId);
}
}
Object.assign(NotificationPopover, {
props: { messageViewLocalId: String },
template: 'mail.NotificationPopover',
});
registerMessagingComponent(NotificationPopover);
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.NotificationPopover" owl="1">
<t t-if="messageView">
<div class="o_NotificationPopover" t-attf-class="{{ className }}" t-ref="root">
<t t-if="messageView.message">
<t t-foreach="messageView.message.notifications" t-as="notification" t-key="notification.localId">
<div class="o_NotificationPopover_notification">
<i class="o_NotificationPopover_notificationIcon me-2" t-att-class="notification.iconClass" t-att-title="notification.iconTitle" role="img"/>
<t t-if="notification.partner">
<span class="o_NotificationPopover_notificationPartnerName" t-esc="notification.partner.nameOrDisplayName"/>
</t>
</div>
</t>
</t>
</div>
</t>
</t>
</templates>
@@ -0,0 +1,14 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class TrackingValue extends Component {}
Object.assign(TrackingValue, {
props: { value: Object },
template: 'mail.TrackingValue',
});
registerMessagingComponent(TrackingValue);
@@ -0,0 +1,6 @@
.o_TrackingValue_item {
// Could be replaced by "me-1" after the migration to BS5
// for the backend AND the frontend
margin-inline-end: map-get($spacers, 1);
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.TrackingValue" owl="1">
<li class="o_TrackingValue" t-attf-class="{{ className }}" t-ref="root">
<div class="o_TrackingValue_container d-flex align-items-center flex-wrap">
<div class="o_TrackingValue_fieldName o_TrackingValue_item" t-esc="props.value.changed_field"/>
<t t-if="props.value.old_value">
<div class="o_TrackingValue_oldValue o_TrackingValue_item" t-esc="props.value.old_value"/>
</t>
<i t-else="" class="mr-2">None</i>
<div class="o_TrackingValue_separator o_TrackingValue_item fa fa-long-arrow-right" title="Changed" role="img"/>
<t t-if="props.value.new_value">
<div class="o_TrackingValue_newValue o_TrackingValue_item" t-esc="props.value.new_value"/>
</t>
<i t-else="">None</i>
</div>
</li>
</t>
</templates>
@@ -7,6 +7,14 @@ import { clear, insertAndReplace } from '@mail/model/model_field_command';
registerModel({
name: 'MessagingMenu',
identifyingFields: ['messaging'],
lifecycleHooks: {
_created() {
document.addEventListener('click', this._onClickCaptureGlobal, true);
},
_willDelete() {
document.removeEventListener('click', this._onClickCaptureGlobal, true);
},
},
recordMethods: {
/**
* Close the messaging menu. Should reset its internal state.
@@ -45,6 +53,38 @@ registerModel({
onHideMobileNewMessage() {
this.update({ isMobileNewMessageToggled: false });
},
/**
* @private
* @param {Event} ev
* @param {Object} ui
* @param {Object} ui.item
* @param {integer} ui.item.id
*/
onMobileNewMessageInputSelect(ev, ui) {
this.messaging.openChat({ partnerId: ui.item.id });
},
/**
* @param {Object} req
* @param {string} req.term
* @param {function} res
*/
onMobileNewMessageInputSource(req, res) {
const value = _.escape(req.term);
this.messaging.models['Partner'].imSearch({
callback: partners => {
const suggestions = partners.map(partner => {
return {
id: partner.id,
value: partner.nameOrDisplayName,
label: partner.nameOrDisplayName,
};
});
res(_.sortBy(suggestions, 'label'));
},
keyword: value,
limit: 10,
});
},
/**
* Toggle the visibility of the messaging menu "new message" input in
* mobile.
@@ -113,6 +153,26 @@ registerModel({
_computeNotificationListView() {
return this.isOpen ? insertAndReplace() : clear();
},
/**
* Closes the menu when clicking outside, if appropriate.
*
* @private
* @param {MouseEvent} ev
*/
_onClickCaptureGlobal(ev) {
if (!this.exists()) {
return;
}
if (!this.component) {
return;
}
// ignore click inside the menu
if (!this.component.root.el || this.component.root.el.contains(ev.target)) {
return;
}
// in all other cases: close the messaging menu when clicking outside
this.close();
},
},
fields: {
/**
@@ -2,6 +2,7 @@
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
registerModel({
name: 'NotificationGroupView',
@@ -30,8 +31,21 @@ registerModel({
onClickMarkAsRead(ev) {
this.notificationGroup.notifyCancel();
},
/**
* @private
* @returns {string|undefined}
*/
_computeImageSrc() {
if (this.notificationGroup.notification_type === 'email') {
return '/mail/static/src/img/smiley/mailfailure.jpg';
}
return clear();
},
},
fields: {
imageSrc: attr({
compute: '_computeImageSrc',
}),
/**
* Reference of the "mark as read" button. Useful to disable the
* top-level click handler when clicking on this specific button.
@@ -7,6 +7,11 @@ import { clear, insertAndReplace, replace } from '@mail/model/model_field_comman
registerModel({
name: 'NotificationListView',
identifyingFields: [['discussOwner', 'messagingMenuOwner']],
lifecycleHooks: {
_created() {
this._loadPreviews();
},
},
recordMethods: {
/**
* @private
@@ -183,6 +188,17 @@ registerModel({
})
);
},
/**
* Load previews of given thread. Basically consists of fetching all missing
* last messages of each thread.
*
* @private
*/
async _loadPreviews() {
const threads = this.threadPreviewViews
.map(threadPreviewView => threadPreviewView.thread);
this.messaging.models['Thread'].loadPreviews(threads);
},
},
fields: {
discussOwner: one('Discuss', {
+1 -1
View File
@@ -1619,7 +1619,7 @@ registerModel({
*/
_computeTypingStatusText() {
if (this.orderedOtherTypingMembers.length === 0) {
return this.constructor.fields.typingStatusText.default;
return clear();
}
if (this.orderedOtherTypingMembers.length === 1) {
return sprintf(
@@ -1492,6 +1492,7 @@ MockServer.include({
history_partner_ids: historyPartnerIds,
needaction_partner_ids: needactionPartnerIds,
notifications,
parentMessage: message.parent_id ? this._mockMailMessageMessageFormat([message.parent_id])[0] : false,
recipients: partners.map(p => ({ id: p.id, name: p.name })),
record_name: thread && (thread.name !== undefined ? thread.name : thread.display_name),
tracking_value_ids: formattedTrackingValues,
@@ -77,7 +77,7 @@ QUnit.test('should be able to search for a new user to invite from an existing c
await click(`.o_ThreadViewTopbar_inviteButton`);
await insertText('.o_ChannelInvitationForm_searchInput', "TestPartner2");
assert.strictEqual(
document.querySelector(`.o_ChannelInvitationForm_selectablePartnerName`).textContent,
document.querySelector(`.o_ChannelInvitationFormSelectablePartner_name`).textContent,
"TestPartner2",
"should display 'TestPartner2' as it matches search term",
);
@@ -117,7 +117,7 @@ QUnit.test('should be able to create a new group chat from an existing chat', as
await click(`.o_ThreadViewTopbar_inviteButton`);
await insertText('.o_ChannelInvitationForm_searchInput', "TestPartner2");
await click(`.o_ChannelInvitationForm_selectablePartnerCheckbox`);
await click(`.o_ChannelInvitationFormSelectablePartner_checkbox`);
await click(`.o_ChannelInvitationForm_inviteButton`);
assert.strictEqual(
document.querySelector(`.o_ThreadViewTopbar_threadName`).textContent,
@@ -2113,11 +2113,11 @@ QUnit.test('chat window does not fetch messages if hidden', async function (asse
await click('.o_ChatWindowHiddenMenu_dropdownToggle');
assert.containsOnce(
document.body,
'.o_ChatWindowHiddenMenu_chatWindowHeader',
'.o_ChatWindowHiddenMenuItem',
"1 hidden chat window should be listed in hidden menu"
);
await click('.o_ChatWindowHiddenMenu_chatWindowHeader');
await click('.o_ChatWindowHiddenMenuItem_chatWindowHeader');
assert.containsN(
document.body,
'.o_ChatWindow',
@@ -207,7 +207,7 @@ QUnit.test('add an emoji', async function (assert) {
});
await createComposerComponent(thread.composer);
await click('.o_Composer_buttonEmojis');
await click('.o_EmojiList_emoji[data-unicode="😊"]');
await click('.o_Emoji[data-unicode="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value,
"😊",
@@ -234,7 +234,7 @@ QUnit.test('add an emoji after some text', async function (assert) {
);
await click('.o_Composer_buttonEmojis');
await click('.o_EmojiList_emoji[data-unicode="😊"]');
await click('.o_Emoji[data-unicode="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value,
"Blabla😊",
@@ -264,7 +264,7 @@ QUnit.test('add emoji replaces (keyboard) text selection', async function (asser
// simulate selection of all the content by keyboard
composerTextInputTextArea.setSelectionRange(0, composerTextInputTextArea.value.length);
await click('.o_Composer_buttonEmojis');
await click('.o_EmojiList_emoji[data-unicode="😊"]');
await click('.o_Emoji[data-unicode="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value,
"😊",
@@ -416,7 +416,7 @@ QUnit.test('add an emoji after a canned response', async function (assert) {
// select emoji
await click('.o_Composer_buttonEmojis');
await click('.o_EmojiList_emoji[data-unicode="😊"]');
await click('.o_Emoji[data-unicode="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"Hello! How are you? 😊",
@@ -564,7 +564,7 @@ QUnit.test('add an emoji after a channel mention', async function (assert) {
// select emoji
await click('.o_Composer_buttonEmojis');
await click('.o_EmojiList_emoji[data-unicode="😊"]');
await click('.o_Emoji[data-unicode="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"#General 😊",
@@ -747,7 +747,7 @@ QUnit.test('add an emoji after a command', async function (assert) {
// select emoji
await click('.o_Composer_buttonEmojis');
await click('.o_EmojiList_emoji[data-unicode="😊"]');
await click('.o_Emoji[data-unicode="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"/who 😊",
@@ -907,7 +907,7 @@ QUnit.test('add an emoji after a partner mention', async function (assert) {
// select emoji
await click('.o_Composer_buttonEmojis');
await click('.o_EmojiList_emoji[data-unicode="😊"]');
await click('.o_Emoji[data-unicode="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"@TestPartner 😊",
@@ -295,7 +295,7 @@ QUnit.test('reply: discard on click away', async function (assert) {
"emoji list should be opened after clicking on emojis button"
);
await click(`.o_EmojiList_emoji`);
await click(`.o_Emoji`);
assert.containsNone(
document.body,
'.o_EmojiList',
@@ -4001,5 +4001,37 @@ QUnit.test('send message only once when enter is pressed twice quickly', async f
);
});
QUnit.test('message being a replied to another message should show message being replied in the message view', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create();
const mailMessageId1 = pyEnv['mail.message'].create({
body: "1st message",
model: 'mail.channel',
res_id: mailChannelId1,
});
const mailMessageId2 = pyEnv['mail.message'].create({
body: "2nd message",
model: 'mail.channel',
parent_id: mailMessageId1,
res_id: mailChannelId1,
});
const { messaging } = await this.start({
discuss: {
context: {
active_id: `mail.channel_${mailChannelId1}`,
},
},
});
assert.containsOnce(
document.querySelector(`.o_Message[data-message-local-id="${
messaging.models['Message'].findFromIdentifyingData({ id: mailMessageId2 }).localId
}"]`),
'.o_MessageInReplyToView',
"message being a replied to another message should show message being replied in the message view",
);
});
});
});
@@ -145,26 +145,26 @@ QUnit.test('Notification Sent', async function (assert) {
await click('.o_Message_notificationIconClickable');
assert.containsOnce(
document.body,
'.o_NotificationPopover',
'.o_MessageNotificationPopoverContent',
"notification popover should be open"
);
assert.containsOnce(
document.body,
'.o_NotificationPopover_notificationIcon',
'.o_MessageNotificationPopoverContent_notificationIcon',
"popover should have one icon"
);
assert.hasClass(
document.querySelector('.o_NotificationPopover_notificationIcon'),
document.querySelector('.o_MessageNotificationPopoverContent_notificationIcon'),
'fa-check',
"popover should have the sent icon"
);
assert.containsOnce(
document.body,
'.o_NotificationPopover_notificationPartnerName',
'.o_MessageNotificationPopoverContent_notificationPartnerName',
"popover should have the partner name"
);
assert.strictEqual(
document.querySelector('.o_NotificationPopover_notificationPartnerName').textContent.trim(),
document.querySelector('.o_MessageNotificationPopoverContent_notificationPartnerName').textContent.trim(),
"Someone",
"partner name should be correct"
);
@@ -728,26 +728,26 @@ QUnit.test('basic rendering of canceled notification', async function (assert) {
await click('.o_Message_notificationIconClickable');
assert.containsOnce(
document.body,
'.o_NotificationPopover',
'.o_MessageNotificationPopoverContent',
"notification popover should be opened after notification has been clicked"
);
assert.containsOnce(
document.body,
'.o_NotificationPopover_notificationIcon',
'.o_MessageNotificationPopoverContent_notificationIcon',
"an icon should be shown in notification popover"
);
assert.containsOnce(
document.body,
'.o_NotificationPopover_notificationIcon.fa.fa-trash-o',
'.o_MessageNotificationPopoverContent_notificationIcon.fa.fa-trash-o',
"the icon shown in notification popover should be the canceled icon"
);
assert.containsOnce(
document.body,
'.o_NotificationPopover_notificationPartnerName',
'.o_MessageNotificationPopoverContent_notificationPartnerName',
"partner name should be shown in notification popover"
);
assert.strictEqual(
document.querySelector('.o_NotificationPopover_notificationPartnerName').textContent.trim(),
document.querySelector('.o_MessageNotificationPopoverContent_notificationPartnerName').textContent.trim(),
"Someone",
"partner name shown in notification popover should be the one concerned by the notification"
);
-1
View File
@@ -46,7 +46,6 @@ The service is provided by the In App Purchase Odoo platform.
'web.assets_backend': [
'sms/static/src/js/fields_phone_widget.js',
'sms/static/src/js/fields_sms_widget.js',
'sms/static/src/components/*/*.js',
'sms/static/src/models/*.js',
],
'web.qunit_suite_tests': [
@@ -1,22 +0,0 @@
/** @odoo-module **/
import { NotificationGroup } from '@mail/components/notification_group/notification_group';
import { patch } from 'web.utils';
patch(NotificationGroup.prototype, 'sms/static/src/components/notification_group/notification_group.js', {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
image() {
if (this.notificationGroupView.notificationGroup.notification_type === 'sms') {
return '/sms/static/img/sms_failure.svg';
}
return this._super(...arguments);
},
});
@@ -0,0 +1,17 @@
/** @odoo-module **/
import { patchRecordMethods } from '@mail/model/model_core';
// ensure that the model definition is loaded before the patch
import '@mail/models/notification_group_view';
patchRecordMethods('NotificationGroupView', {
/**
* @override
*/
_computeImageSrc() {
if (this.notificationGroup.notification_type === 'sms') {
return '/sms/static/img/sms_failure.svg';
}
return this._super();
},
});
@@ -69,26 +69,26 @@ QUnit.test('Notification Sent', async function (assert) {
});
assert.containsOnce(
document.body,
'.o_NotificationPopover',
'.o_MessageNotificationPopoverContent',
"notification popover should be open"
);
assert.containsOnce(
document.body,
'.o_NotificationPopover_notificationIcon',
'.o_MessageNotificationPopoverContent_notificationIcon',
"popover should have one icon"
);
assert.hasClass(
document.querySelector('.o_NotificationPopover_notificationIcon'),
document.querySelector('.o_MessageNotificationPopoverContent_notificationIcon'),
'fa-check',
"popover should have the sent icon"
);
assert.containsOnce(
document.body,
'.o_NotificationPopover_notificationPartnerName',
'.o_MessageNotificationPopoverContent_notificationPartnerName',
"popover should have the partner name"
);
assert.strictEqual(
document.querySelector('.o_NotificationPopover_notificationPartnerName').textContent.trim(),
document.querySelector('.o_MessageNotificationPopoverContent_notificationPartnerName').textContent.trim(),
"Someone",
"partner name should be correct"
);
@@ -20,7 +20,7 @@
It was decided that the information displayed for snailmail messages
has to be different than for standard messages, see task-1907998.
-->
<xpath expr="//NotificationPopover" position="replace">
<xpath expr="//MessageNotificationPopoverContent" position="replace">
<t t-if="messageView.message.message_type === 'snailmail'">
<SnailmailNotificationPopover messageLocalId="messageView.message.localId"/>
</t>
@@ -1,22 +0,0 @@
/** @odoo-module **/
import { NotificationGroup } from '@mail/components/notification_group/notification_group';
import { patch } from 'web.utils';
patch(NotificationGroup.prototype, 'snailmail/static/src/components/notification_group/notification_group.js', {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
image() {
if (this.notificationGroupView.notificationGroup.notification_type === 'snail') {
return '/snailmail/static/img/snailmail_failure.png';
}
return this._super(...arguments);
},
});
@@ -0,0 +1,17 @@
/** @odoo-module **/
import { patchRecordMethods } from '@mail/model/model_core';
// ensure that the model definition is loaded before the patch
import '@mail/models/notification_group_view';
patchRecordMethods('NotificationGroupView', {
/**
* @override
*/
_computeImageSrc() {
if (this.notificationGroup.notification_type === 'snail') {
return '/snailmail/static/img/snailmail_failure.png';
}
return this._super();
},
});
@@ -52,41 +52,41 @@ QUnit.test('basic rendering of tracking value (float type)', async function (ass
await click('.o_form_button_save');
assert.containsOnce(
document.body,
'.o_Message_trackingValue',
'.o_TrackingValue',
"should display a tracking value"
);
assert.containsOnce(
document.body,
'.o_Message_trackingValueFieldName',
'.o_TrackingValue_fieldName',
"should display the name of the tracked field"
);
assert.strictEqual(
document.querySelector('.o_Message_trackingValueFieldName').textContent,
document.querySelector('.o_TrackingValue_fieldName').textContent,
"Float:",
"should display the correct tracked field name (Float)",
);
assert.containsOnce(
document.body,
'.o_Message_trackingValueOldValue',
'.o_TrackingValue_oldValue',
"should display the old value"
);
assert.strictEqual(
document.querySelector('.o_Message_trackingValueOldValue').textContent,
document.querySelector('.o_TrackingValue_oldValue').textContent,
"12.30",
"should display the correct old value (12.30)",
);
assert.containsOnce(
document.body,
'.o_Message_trackingValueSeparator',
'.o_TrackingValue_separator',
"should display the separator"
);
assert.containsOnce(
document.body,
'.o_Message_trackingValueNewValue',
'.o_TrackingValue_newValue',
"should display the new value"
);
assert.strictEqual(
document.querySelector('.o_Message_trackingValueNewValue').textContent,
document.querySelector('.o_TrackingValue_newValue').textContent,
"45.67",
"should display the correct new value (45.67)",
);
@@ -102,7 +102,7 @@ QUnit.test('rendering of tracked field of type float: from non-0 to 0', async fu
await testUtils.fields.editInput(form.$('input[name=float_field]'), 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Float:1.000.00",
"should display the correct content of tracked field of type float: from non-0 to 0 (Float: 1.00 -> 0.00)"
);
@@ -118,7 +118,7 @@ QUnit.test('rendering of tracked field of type float: from 0 to non-0', async fu
await testUtils.fields.editInput(form.$('input[name=float_field]'), 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Float:0.001.00",
"should display the correct content of tracked field of type float: from 0 to non-0 (Float: 0.00 -> 1.00)"
);
@@ -134,7 +134,7 @@ QUnit.test('rendering of tracked field of type integer: from non-0 to 0', async
await testUtils.fields.editInput(form.$('input[name=integer_field]'), 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Integer:10",
"should display the correct content of tracked field of type integer: from non-0 to 0 (Integer: 1 -> 0)"
);
@@ -150,7 +150,7 @@ QUnit.test('rendering of tracked field of type integer: from 0 to non-0', async
await testUtils.fields.editInput(form.$('input[name=integer_field]'), 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Integer:01",
"should display the correct content of tracked field of type integer: from 0 to non-0 (Integer: 0 -> 1)"
);
@@ -166,7 +166,7 @@ QUnit.test('rendering of tracked field of type monetary: from non-0 to 0', async
await testUtils.fields.editSelect(form.$('div[name=monetary_field] > input'), 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Monetary:1.000.00",
"should display the correct content of tracked field of type monetary: from non-0 to 0 (Monetary: 1.00 -> 0.00)"
);
@@ -182,7 +182,7 @@ QUnit.test('rendering of tracked field of type monetary: from 0 to non-0', async
await testUtils.fields.editSelect(form.$('div[name=monetary_field] > input'), 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Monetary:0.001.00",
"should display the correct content of tracked field of type monetary: from 0 to non-0 (Monetary: 0.00 -> 1.00)"
);
@@ -198,7 +198,7 @@ QUnit.test('rendering of tracked field of type boolean: from true to false', asy
form.$('.custom-checkbox input').click();
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Boolean:YesNo",
"should display the correct content of tracked field of type boolean: from true to false (Boolean: True -> False)"
);
@@ -214,7 +214,7 @@ QUnit.test('rendering of tracked field of type boolean: from false to true', asy
form.$('.custom-checkbox input').click();
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Boolean:NoYes",
"should display the correct content of tracked field of type boolean: from false to true (Boolean: False -> True)"
);
@@ -230,7 +230,7 @@ QUnit.test('rendering of tracked field of type char: from a string to empty stri
await testUtils.fields.editInput(form.$('input[name=char_field]'), '');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Char:MarcNone",
"should display the correct content of tracked field of type char: from a string to empty string (Char: Marc -> None)"
);
@@ -246,7 +246,7 @@ QUnit.test('rendering of tracked field of type char: from empty string to a stri
await testUtils.fields.editInput(form.$('input[name=char_field]'), 'Marc');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Char:NoneMarc",
"should display the correct content of tracked field of type char: from empty string to a string (Char: None -> Marc)"
);
@@ -262,7 +262,7 @@ QUnit.test('rendering of tracked field of type date: from no date to a set date'
await testUtils.fields.editAndTrigger(form.$('.o_datepicker[name=date_field] .o_datepicker_input'), '12/14/2018', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Date:None12/14/2018",
"should display the correct content of tracked field of type date: from no date to a set date (Date: None -> 12/14/2018)"
);
@@ -278,7 +278,7 @@ QUnit.test('rendering of tracked field of type date: from a set date to no date'
await testUtils.fields.editAndTrigger(form.$('.o_datepicker[name=date_field] .o_datepicker_input'), '', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Date:12/14/2018None",
"should display the correct content of tracked field of type date: from a set date to no date (Date: 12/14/2018 -> None)"
);
@@ -294,7 +294,7 @@ QUnit.test('rendering of tracked field of type datetime: from no date and time t
await testUtils.fields.editAndTrigger(form.$('.o_datepicker[name=datetime_field] .o_datepicker_input'), '12/14/2018 13:42:28', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Datetime:None12/14/2018 13:42:28",
"should display the correct content of tracked field of type datetime: from no date and time to a set date and time (Datetime: None -> 12/14/2018 13:42:28)"
);
@@ -310,7 +310,7 @@ QUnit.test('rendering of tracked field of type datetime: from a set date and tim
await testUtils.fields.editAndTrigger(form.$('.o_datepicker[name=datetime_field] .o_datepicker_input'), '', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Datetime:12/14/2018 13:42:28None",
"should display the correct content of tracked field of type datetime: from a set date and time to no date and time (Datetime: 12/14/2018 13:42:28 -> None)"
);
@@ -326,7 +326,7 @@ QUnit.test('rendering of tracked field of type text: from some text to empty', a
await testUtils.fields.editInput(form.$('textarea[name=text_field]'), '');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Text:MarcNone",
"should display the correct content of tracked field of type text: from some text to empty (Text: Marc -> None)"
);
@@ -342,7 +342,7 @@ QUnit.test('rendering of tracked field of type text: from empty to some text', a
await testUtils.fields.editInput(form.$('textarea[name=text_field]'), 'Marc');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Text:NoneMarc",
"should display the correct content of tracked field of type text: from empty to some text (Text: None -> Marc)"
);
@@ -358,7 +358,7 @@ QUnit.test('rendering of tracked field of type selection: from a selection to no
await testUtils.fields.editSelect(form.$('select[name=selection_field]'), '');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Selection:firstNone",
"should display the correct content of tracked field of type selection: from a selection to no selection (Selection: first -> None)"
);
@@ -374,7 +374,7 @@ QUnit.test('rendering of tracked field of type selection: from no selection to a
await testUtils.fields.editSelect(form.$('select[name=selection_field]'), '"first"');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Selection:Nonefirst",
"should display the correct content of tracked field of type selection: from no selection to a selection (Selection: None -> first)"
);
@@ -391,7 +391,7 @@ QUnit.test('rendering of tracked field of type many2one: from having a related r
await testUtils.fields.editAndTrigger(form.$('.o_field_many2one_selection input'), '', ['keyup']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Many2one:MarcNone",
"should display the correct content of tracked field of type many2one: from having a related record to no related record (Many2one: Marc -> None)"
);
@@ -409,7 +409,7 @@ QUnit.test('rendering of tracked field of type many2one: from no related record
await testUtils.fields.many2one.clickItem('many2one_field_id', 'Marc');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_Message_trackingValue').textContent,
document.querySelector('.o_TrackingValue').textContent,
"Many2one:NoneMarc",
"should display the correct content of tracked field of type many2one: from no related record to having a related record (Many2one: None -> Marc)"
);