[IMP] mail: move component handlers to models (step 5)
This commit moves some handler methods from components to models, as a step closer to having most of business code in models. Having business code in models is desirable so that the code is much more maintainable: easier to change and more robust code. Task-2579306 closes odoo/odoo#88878 Related: odoo/enterprise#26277 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
import Popover from "web.Popover";
|
||||
@@ -7,6 +8,14 @@ import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
export class Activity extends LegacyComponent {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component', modelName: 'ActivityView' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ActivityView}
|
||||
*/
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<FollowerListMenu
|
||||
className="'o_ChatterTopbar_followerListMenu'"
|
||||
isDisabled="chatter.isDisabled"
|
||||
localId="chatter.followerListMenuView.localId"
|
||||
threadLocalId="chatter.thread.localId"
|
||||
isChatterButton="true"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { useUpdate } from '@mail/component_hooks/use_update';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
@@ -14,7 +13,6 @@ export class ComposerTextInput extends Component {
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'textInputComponent', modelName: 'ComposerView' });
|
||||
useRefToModel({ fieldName: 'mirroredTextareaRef', modelName: 'ComposerView', refName: 'mirroredTextarea' });
|
||||
useRefToModel({ fieldName: 'textareaRef', modelName: 'ComposerView', refName: 'textarea' });
|
||||
/**
|
||||
@@ -40,18 +38,6 @@ export class ComposerTextInput extends Component {
|
||||
return this.messaging && this.messaging.models['ComposerView'].get(this.props.localId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the composer text input state in store
|
||||
*/
|
||||
saveStateInStore() {
|
||||
this.composerView.composer.update({
|
||||
textInputContent: this.composerView.textareaRef.el.value,
|
||||
textInputCursorEnd: this.composerView.textareaRef.el.selectionEnd,
|
||||
textInputCursorStart: this.composerView.textareaRef.el.selectionStart,
|
||||
textInputSelectionDirection: this.composerView.textareaRef.el.selectionDirection,
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -113,28 +99,6 @@ export class ComposerTextInput extends Component {
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onClickTextarea() {
|
||||
if (!this.composerView) {
|
||||
return;
|
||||
}
|
||||
// clicking might change the cursor position
|
||||
this.saveStateInStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onFocusoutTextarea() {
|
||||
if (!this.composerView) {
|
||||
return;
|
||||
}
|
||||
this.saveStateInStore();
|
||||
this.composerView.update({ isFocused: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -142,7 +106,7 @@ export class ComposerTextInput extends Component {
|
||||
if (!this.composerView) {
|
||||
return;
|
||||
}
|
||||
this.saveStateInStore();
|
||||
this.composerView.saveStateInStore();
|
||||
if (this._textareaLastInputValue !== this.composerView.textareaRef.el.value) {
|
||||
this.composerView.handleCurrentPartnerIsTyping();
|
||||
}
|
||||
@@ -150,95 +114,6 @@ export class ComposerTextInput extends Component {
|
||||
this._updateHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Key events management is performed in a Keyup to avoid intempestive RPC calls
|
||||
*
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onKeyupTextarea(ev) {
|
||||
if (!this.composerView) {
|
||||
return;
|
||||
}
|
||||
switch (ev.key) {
|
||||
case 'Escape':
|
||||
// already handled in _onKeydownTextarea, break to avoid default
|
||||
break;
|
||||
// ENTER, HOME, END, UP, DOWN, PAGE UP, PAGE DOWN, TAB: check if navigation in mention suggestions
|
||||
case 'Enter':
|
||||
if (this.composerView.hasSuggestions) {
|
||||
this.composerView.insertSuggestion();
|
||||
this.composerView.closeSuggestions();
|
||||
this.composerView.update({ doFocus: true });
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'PageUp':
|
||||
if (ev.key === 'ArrowUp' && !this.composerView.hasSuggestions && !this.composerView.composer.textInputContent && this.composerView.threadView) {
|
||||
this.composerView.threadView.startEditingLastMessageFromCurrentUser();
|
||||
break;
|
||||
}
|
||||
if (this.composerView.hasSuggestions) {
|
||||
this.composerView.setPreviousSuggestionActive();
|
||||
this.composerView.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 'PageDown':
|
||||
if (ev.key === 'ArrowDown' && !this.composerView.hasSuggestions && !this.composerView.composer.textInputContent && this.composerView.threadView) {
|
||||
this.composerView.threadView.startEditingLastMessageFromCurrentUser();
|
||||
break;
|
||||
}
|
||||
if (this.composerView.hasSuggestions) {
|
||||
this.composerView.setNextSuggestionActive();
|
||||
this.composerView.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
if (this.composerView.hasSuggestions) {
|
||||
this.composerView.setFirstSuggestionActive();
|
||||
this.composerView.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
if (this.composerView.hasSuggestions) {
|
||||
this.composerView.setLastSuggestionActive();
|
||||
this.composerView.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
break;
|
||||
case 'Tab':
|
||||
if (this.composerView.hasSuggestions) {
|
||||
if (ev.shiftKey) {
|
||||
this.composerView.setPreviousSuggestionActive();
|
||||
this.composerView.update({ hasToScrollToActiveSuggestion: true });
|
||||
} else {
|
||||
this.composerView.setNextSuggestionActive();
|
||||
this.composerView.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Alt':
|
||||
case 'AltGraph':
|
||||
case 'CapsLock':
|
||||
case 'Control':
|
||||
case 'Fn':
|
||||
case 'FnLock':
|
||||
case 'Hyper':
|
||||
case 'Meta':
|
||||
case 'NumLock':
|
||||
case 'ScrollLock':
|
||||
case 'Shift':
|
||||
case 'ShiftSuper':
|
||||
case 'Symbol':
|
||||
case 'SymbolLock':
|
||||
// prevent modifier keys from resetting the suggestion state
|
||||
break;
|
||||
// Otherwise, check if a mention is typed
|
||||
default:
|
||||
this.saveStateInStore();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ComposerTextInput, {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
isBelow="props.hasMentionSuggestionsBelowPosition"
|
||||
/>
|
||||
</t>
|
||||
<textarea class="o_ComposerTextInput_textarea o_ComposerTextInput_textareaStyle" t-att-class="{ 'o-composer-is-compact': composerView.isCompact }" t-esc="composerView.composer.textInputContent" t-att-placeholder="composerView.composer.placeholder" t-on-click="_onClickTextarea" t-on-focusin="composerView.onFocusinTextarea" t-on-focusout="_onFocusoutTextarea" t-on-keydown="composerView.onKeydownTextarea" t-on-keyup="_onKeyupTextarea" t-on-input="_onInputTextarea" t-ref="textarea"/>
|
||||
<textarea class="o_ComposerTextInput_textarea o_ComposerTextInput_textareaStyle" t-att-class="{ 'o-composer-is-compact': composerView.isCompact }" t-esc="composerView.composer.textInputContent" t-att-placeholder="composerView.composer.placeholder" t-on-click="composerView.onClickTextarea" t-on-focusin="composerView.onFocusinTextarea" t-on-focusout="composerView.onFocusoutTextarea" t-on-keydown="composerView.onKeydownTextarea" t-on-keyup="composerView.onKeyupTextarea" t-on-input="_onInputTextarea" t-ref="textarea"/>
|
||||
<!--
|
||||
This is an invisible textarea used to compute the composer
|
||||
height based on the text content. We need it to downsize
|
||||
|
||||
@@ -21,25 +21,6 @@ export class Follower extends Component {
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickDetails(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.follower.openProfile();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickEdit(ev) {
|
||||
ev.preventDefault();
|
||||
this.follower.showSubtypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<t t-name="mail.Follower" owl="1">
|
||||
<t t-if="follower">
|
||||
<div class="o_Follower d-flex justify-content-between p-0" t-attf-class="{{ className }}" t-on-click="props.onClick" t-ref="root">
|
||||
<a class="o_Follower_details d-flex align-items-center flex-grow-1 px-3 text-700" t-att-class="{ 'o-inactive font-italic opacity-25': !follower.isActive }" href="#" t-on-click="_onClickDetails">
|
||||
<a class="o_Follower_details d-flex align-items-center flex-grow-1 px-3 text-700" t-att-class="{ 'o-inactive font-italic opacity-25': !follower.isActive }" href="#" t-on-click="follower.onClickDetails">
|
||||
<img class="o_Follower_avatar me-2 rounded-circle" t-att-src="follower.partner.avatarUrl" alt="Avatar"/>
|
||||
<span class="o_Follower_name flex-shrink text-truncate" t-esc="follower.partner.nameOrDisplayName"/>
|
||||
</a>
|
||||
<t t-if="follower.isEditable">
|
||||
<button class="o_Follower_button o_Follower_editButton btn btn-icon rounded-0" title="Edit subscription" t-on-click="_onClickEdit">
|
||||
<button class="o_Follower_button o_Follower_editButton btn btn-icon rounded-0" title="Edit subscription" t-on-click="follower.onClickEdit">
|
||||
<i class="fa fa-pencil"/>
|
||||
</button>
|
||||
<button class="o_Follower_button o_Follower_removeButton btn btn-icon rounded-0" title="Remove this follower" t-on-click="_onClickRemove">
|
||||
|
||||
@@ -36,6 +36,13 @@ export class FollowerListMenu extends Component {
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @return {FollowerListMenuView}
|
||||
*/
|
||||
get followerListMenuView() {
|
||||
return this.messaging && this.messaging.models['FollowerListMenuView'].get(this.props.localId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Thread}
|
||||
*/
|
||||
@@ -121,6 +128,7 @@ Object.assign(FollowerListMenu, {
|
||||
isDisabled: { type: Boolean, optional: true },
|
||||
threadLocalId: String,
|
||||
isChatterButton: { type: Boolean, optional: true },
|
||||
localId: String,
|
||||
},
|
||||
template: 'mail.FollowerListMenu',
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.FollowerListMenu" owl="1">
|
||||
<t t-if="thread">
|
||||
<t t-if="followerListMenuView and thread">
|
||||
<div class="o_FollowerListMenu position-relative d-flex" t-attf-class="{{ className }}" t-on-keydown="_onKeydown" t-ref="root">
|
||||
<div class="o_FollowerListMenu_followers d-flex" t-ref="dropdown">
|
||||
<button class="o_FollowerListMenu_buttonFollowers btn btn-link" t-att-disabled="props.isDisabled" t-on-click="_onClickFollowersButton" title="Show Followers" t-att-class="{ 'o_ChatterTopbar_button': props.isChatterButton }">
|
||||
|
||||
@@ -17,24 +17,6 @@ export class FollowerSubtype extends Component {
|
||||
return this.messaging && this.messaging.models['FollowerSubtypeView'].get(this.props.localId);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when clicking on cancel button.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onChangeCheckbox(ev) {
|
||||
if (ev.target.checked) {
|
||||
this.followerSubtypeView.follower.selectSubtype(this.followerSubtypeView.subtype);
|
||||
} else {
|
||||
this.followerSubtypeView.follower.unselectSubtype(this.followerSubtypeView.subtype);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(FollowerSubtype, {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<t t-if="followerSubtypeView">
|
||||
<div class="o_FollowerSubtype" t-attf-class="{{ className }}" t-att-data-local-id="followerSubtypeView.localId" t-ref="root">
|
||||
<label class="o_FollowerSubtype_label flex-grow-1 align-items-center d-flex mb-0 p-2 o_cursor_pointer">
|
||||
<input class="o_FollowerSubtype_checkbox me-2" type="checkbox" t-att-checked="followerSubtypeView.follower.selectedSubtypes.includes(followerSubtypeView.subtype) ? 'checked': ''" t-on-change="_onChangeCheckbox"/>
|
||||
<input class="o_FollowerSubtype_checkbox me-2" type="checkbox" t-att-checked="followerSubtypeView.follower.selectedSubtypes.includes(followerSubtypeView.subtype) ? 'checked': ''" t-on-change="followerSubtypeView.onChangeCheckbox"/>
|
||||
<t t-esc="followerSubtypeView.subtype.name"/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -26,30 +26,6 @@ export class FollowerSubtypeList extends Component {
|
||||
return this.messaging && this.messaging.models['FollowerSubtypeList'].get(this.props.localId);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when clicking on cancel button.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickCancel(ev) {
|
||||
this.followerSubtypeList.follower.closeSubtypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when clicking on apply button.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickApply(ev) {
|
||||
this.followerSubtypeList.follower.updateSubtypes();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(FollowerSubtypeList, {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<h4 class="modal-title">
|
||||
Edit Subscription of <t t-esc="followerSubtypeList.follower.partner.nameOrDisplayName"/>
|
||||
</h4>
|
||||
<i class="o_FollowerSubtypeList_closeButton close fa fa-times" aria-label="Close" t-on-click="_onClickCancel"/>
|
||||
<i class="o_FollowerSubtypeList_closeButton close fa fa-times" aria-label="Close" t-on-click="followerSubtypeList.onClickCancel"/>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<div class="o_FollowerSubtypeList_subtypes flex-column d-flex">
|
||||
@@ -22,10 +22,10 @@
|
||||
</div>
|
||||
</main>
|
||||
<div class="modal-footer">
|
||||
<button class="o-apply btn btn-primary" t-on-click="_onClickApply">
|
||||
<button class="o-apply btn btn-primary" t-on-click="followerSubtypeList.onClickApply">
|
||||
Apply
|
||||
</button>
|
||||
<button class="o-cancel btn btn-secondary" t-on-click="_onClickCancel">
|
||||
<button class="o-cancel btn btn-secondary" t-on-click="followerSubtypeList.onClickCancel">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -17,30 +17,6 @@ export class MailTemplate extends Component {
|
||||
return this.messaging && this.messaging.models['MailTemplateView'].get(this.props.localId);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickPreview(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.mailTemplateView.mailTemplate.preview(this.mailTemplateView.activityViewOwner.activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickSend(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.mailTemplateView.mailTemplate.send(this.mailTemplateView.activityViewOwner.activity);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(MailTemplate, {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button
|
||||
class="o_MailTemplate_button o_MailTemplate_preview btn btn-link py-0"
|
||||
t-att-data-mail-template-id="mailTemplateView.mailTemplate.id"
|
||||
t-on-click="_onClickPreview"
|
||||
t-on-click="mailTemplateView.onClickPreview"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
@@ -18,7 +18,7 @@
|
||||
<button
|
||||
class="o_MailTemplate_button o_MailTemplate_send btn btn-link py-0"
|
||||
t-att-data-mail-template-id="mailTemplateView.mailTemplate.id"
|
||||
t-on-click="_onClickSend"
|
||||
t-on-click="mailTemplateView.onClickSend"
|
||||
>
|
||||
Send Now
|
||||
</button>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useComponentToModel } from '@mail/component_hooks/use_component_to_mode
|
||||
import { useUpdate } from '@mail/component_hooks/use_update';
|
||||
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
import { isEventHandled, markEventHandled } from '@mail/utils/utils';
|
||||
|
||||
import { _lt } from 'web.core';
|
||||
import Popover from "web.Popover";
|
||||
@@ -275,80 +274,6 @@ export class Message extends Component {
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClick(ev) {
|
||||
if (ev.target.closest('.o_channel_redirect')) {
|
||||
this.messaging.openProfile({
|
||||
id: Number(ev.target.dataset.oeId),
|
||||
model: 'mail.channel',
|
||||
});
|
||||
// avoid following dummy href
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (ev.target.tagName === 'A') {
|
||||
if (ev.target.dataset.oeId && ev.target.dataset.oeModel) {
|
||||
this.messaging.openProfile({
|
||||
id: Number(ev.target.dataset.oeId),
|
||||
model: ev.target.dataset.oeModel,
|
||||
});
|
||||
// avoid following dummy href
|
||||
ev.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!isEventHandled(ev, 'Message.ClickAuthorAvatar') &&
|
||||
!isEventHandled(ev, 'Message.ClickAuthorName') &&
|
||||
!isEventHandled(ev, 'Message.ClickFailure') &&
|
||||
!isEventHandled(ev, 'MessageActionList.Click') &&
|
||||
!isEventHandled(ev, 'MessageReactionGroup.Click') &&
|
||||
!isEventHandled(ev, 'MessageInReplyToView.ClickMessageInReplyTo')
|
||||
) {
|
||||
this.messageView.update({ isClicked: !this.messageView.isClicked });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickAuthorAvatar(ev) {
|
||||
markEventHandled(ev, 'Message.ClickAuthorAvatar');
|
||||
if (!this.messageView.hasAuthorOpenChat) {
|
||||
return;
|
||||
}
|
||||
this.messageView.message.author.openChat();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickAuthorName(ev) {
|
||||
markEventHandled(ev, 'Message.ClickAuthorName');
|
||||
if (!this.messageView.message.author) {
|
||||
return;
|
||||
}
|
||||
this.messageView.message.author.openProfile();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickOriginThread(ev) {
|
||||
// avoid following dummy href
|
||||
ev.preventDefault();
|
||||
this.messageView.message.originThread.open();
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(Message, {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
'o-squashed': messageView.isSquashed,
|
||||
'o-starred': messageView.message.isStarred,
|
||||
'mt-3': !messageView.isSquashed and messageView.threadView,
|
||||
}" t-attf-class="{{ messageView.extraClass }} {{ className }}" t-on-click="_onClick" t-on-mouseenter="messageView.onMouseenter" t-on-mouseleave="messageView.onMouseleave" t-att-data-message-local-id="messageView.message.localId"
|
||||
}" 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"/>
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="o_Message_sidebar" t-att-class="{ 'o-message-squashed align-items-start': messageView.isSquashed }">
|
||||
<t t-if="!messageView.isSquashed">
|
||||
<div class="o_Message_authorAvatarContainer o_Message_sidebarItem">
|
||||
<img class="o_Message_authorAvatar rounded-circle" t-att-class="{ o_Message_authorRedirect: messageView.hasAuthorOpenChat, o_redirect: messageView.hasAuthorOpenChat }" t-att-src="messageView.message.avatarUrl" t-on-click="_onClickAuthorAvatar" t-att-title="messageView.hasAuthorOpenChat ? messageView.authorAvatarTitleText : ''" alt="Avatar"/>
|
||||
<img class="o_Message_authorAvatar rounded-circle" t-att-class="{ o_Message_authorRedirect: messageView.hasAuthorOpenChat, o_redirect: messageView.hasAuthorOpenChat }" t-att-src="messageView.message.avatarUrl" t-on-click="messageView.onClickAuthorAvatar" t-att-title="messageView.hasAuthorOpenChat ? messageView.authorAvatarTitleText : ''" alt="Avatar"/>
|
||||
<t t-if="messageView.message.author and messageView.message.author.im_status">
|
||||
<PartnerImStatusIcon
|
||||
className="'o_Message_partnerImStatusIcon'"
|
||||
@@ -58,7 +58,7 @@
|
||||
<t t-if="!messageView.isSquashed">
|
||||
<div class="o_Message_header ml-2">
|
||||
<t t-if="messageView.message.author">
|
||||
<div class="o_Message_authorName o_Message_authorRedirect o_redirect text-truncate" t-on-click="_onClickAuthorName" title="Open profile">
|
||||
<div class="o_Message_authorName o_Message_authorRedirect o_redirect text-truncate" t-on-click="messageView.onClickAuthorName" title="Open profile">
|
||||
<t t-if="messageView.message.originThread">
|
||||
<t t-esc="messageView.message.originThread.getMemberName(messageView.message.author)"/>
|
||||
</t>
|
||||
@@ -93,10 +93,10 @@
|
||||
<t t-if="messageView.threadView and messageView.message.originThread and messageView.message.originThread !== messageView.threadView.thread">
|
||||
<div class="o_Message_originThread" t-att-class="{ 'o-message-selected': messageView.isSelected }">
|
||||
<t t-if="messageView.message.originThread.model === 'mail.channel'">
|
||||
(from <a class="o_Message_originThreadLink" t-att-href="messageView.message.originThread.url" t-on-click="_onClickOriginThread"><t t-if="messageView.message.originThread.displayName">#<t t-esc="messageView.message.originThread.displayName"/></t><t t-else="">channel</t></a>)
|
||||
(from <a class="o_Message_originThreadLink" t-att-href="messageView.message.originThread.url" t-on-click="messageView.onClickOriginThread"><t t-if="messageView.message.originThread.displayName">#<t t-esc="messageView.message.originThread.displayName"/></t><t t-else="">channel</t></a>)
|
||||
</t>
|
||||
<t t-else="">
|
||||
on <a class="o_Message_originThreadLink" t-att-href="messageView.message.originThread.url" t-on-click="_onClickOriginThread"><t t-if="messageView.message.originThread.displayName"><t t-esc="messageView.message.originThread.displayName"/></t><t t-else="">document</t></a>
|
||||
on <a class="o_Message_originThreadLink" t-att-href="messageView.message.originThread.url" t-on-click="messageView.onClickOriginThread"><t t-if="messageView.message.originThread.displayName"><t t-esc="messageView.message.originThread.displayName"/></t><t t-else="">document</t></a>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -67,40 +67,6 @@ export class MessagingMenu extends Component {
|
||||
this.messagingMenu.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickDesktopTabButton(ev) {
|
||||
this.messagingMenu.update({ activeTabId: ev.currentTarget.dataset.tabId });
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickNewMessage(ev) {
|
||||
if (!this.messaging.device.isMobile) {
|
||||
this.messaging.chatWindowManager.openNewMessage();
|
||||
this.messagingMenu.close();
|
||||
} else {
|
||||
this.messagingMenu.toggleMobileNewMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickToggler(ev) {
|
||||
// avoid following dummy href
|
||||
ev.preventDefault();
|
||||
if (!this.messagingMenu) {
|
||||
return;
|
||||
}
|
||||
this.messagingMenu.toggleOpen();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<t t-name="mail.MessagingMenu" owl="1">
|
||||
<t t-if="messagingMenu">
|
||||
<div class="o_MessagingMenu dropdown" t-att-class="{ 'show': messagingMenu.isOpen, 'o-mobile': messaging.device.isMobile }" t-attf-class="{{ className }}" t-ref="root">
|
||||
<a class="o_MessagingMenu_toggler dropdown-toggle o-no-caret o-dropdown--narrow" t-att-class="{ 'o-no-notification': !messagingMenu.counter }" href="#" title="Conversations" role="button" t-att-aria-expanded="messagingMenu.isOpen ? 'true' : 'false'" aria-haspopup="true" t-on-click="_onClickToggler">
|
||||
<a class="o_MessagingMenu_toggler dropdown-toggle o-no-caret o-dropdown--narrow" t-att-class="{ 'o-no-notification': !messagingMenu.counter }" href="#" title="Conversations" role="button" t-att-aria-expanded="messagingMenu.isOpen ? 'true' : 'false'" aria-haspopup="true" t-on-click="messagingMenu.onClickToggler">
|
||||
<i class="o_MessagingMenu_icon fa fa-lg fa-comments" role="img" aria-label="Messages"/>
|
||||
<t t-if="!messaging.isInitialized">
|
||||
<i class="o_MessagingMenu_loading fa fa-circle-o-notch fa-spin"/>
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="o_MessagingMenu_dropdownMenuHeader border-bottom" t-att-class="{ 'o-mobile': messaging.device.isMobile }">
|
||||
<t t-if="!messaging.device.isMobile">
|
||||
<t t-foreach="['all', 'chat', 'channel']" t-as="tabId" t-key="tabId">
|
||||
<button class="o_MessagingMenu_tabButton o-desktop btn btn-link" t-att-class="{ 'o-active': messagingMenu.activeTabId === tabId, }" t-on-click="_onClickDesktopTabButton" type="button" role="tab" t-att-data-tab-id="tabId">
|
||||
<button class="o_MessagingMenu_tabButton o-desktop btn btn-link" t-att-class="{ 'o-active': messagingMenu.activeTabId === tabId, }" t-on-click="messagingMenu.onClickDesktopTabButton" type="button" role="tab" t-att-data-tab-id="tabId">
|
||||
<t t-if="tabId === 'all'">All</t>
|
||||
<t t-elif="tabId === 'chat'">Chat</t>
|
||||
<t t-elif="tabId === 'channel'">Channels</t>
|
||||
@@ -72,7 +72,7 @@
|
||||
'btn-link': !messaging.device.isMobile,
|
||||
'btn-secondary': messaging.device.isMobile,
|
||||
'o-mobile': messaging.device.isMobile,
|
||||
}" t-on-click="_onClickNewMessage" type="button"
|
||||
}" t-on-click="messagingMenu.onClickNewMessage" type="button"
|
||||
>
|
||||
New message
|
||||
</button>
|
||||
|
||||
@@ -190,6 +190,10 @@ registerModel({
|
||||
inverse: 'activityViewOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
/**
|
||||
* States the OWL component of this activity view.
|
||||
*/
|
||||
component: attr(),
|
||||
/**
|
||||
* Compute the label for "when" the activity is due.
|
||||
*/
|
||||
|
||||
@@ -144,6 +144,16 @@ registerModel({
|
||||
}
|
||||
return clear();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {FieldCommand}
|
||||
*/
|
||||
_computeFollowerListMenuView() {
|
||||
if (this.hasFollowers && this.thread) {
|
||||
return insertAndReplace();
|
||||
}
|
||||
return clear();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
@@ -282,6 +292,11 @@ registerModel({
|
||||
context: attr({
|
||||
default: {},
|
||||
}),
|
||||
followerListMenuView: one('FollowerListMenuView', {
|
||||
compute: '_computeFollowerListMenuView',
|
||||
inverse: 'chatterOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
/**
|
||||
* Determines whether `this` should display an activity box.
|
||||
*/
|
||||
|
||||
@@ -227,9 +227,7 @@ registerModel({
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickEmoji(ev) {
|
||||
if (this.textInputComponent) {
|
||||
this.textInputComponent.saveStateInStore();
|
||||
}
|
||||
this.saveStateInStore();
|
||||
this.insertIntoTextInput(ev.currentTarget.dataset.unicode);
|
||||
if (!this.messaging.device.isMobileDevice) {
|
||||
this.update({ doFocus: true });
|
||||
@@ -290,12 +288,26 @@ registerModel({
|
||||
this.closeSuggestions();
|
||||
this.update({ doFocus: true });
|
||||
},
|
||||
onClickTextarea() {
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
}
|
||||
// clicking might change the cursor position
|
||||
this.saveStateInStore();
|
||||
},
|
||||
onFocusinTextarea() {
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
}
|
||||
this.update({ isFocused: true });
|
||||
},
|
||||
onFocusoutTextarea() {
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
}
|
||||
this.saveStateInStore();
|
||||
this.update({ isFocused: false });
|
||||
},
|
||||
/**
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
@@ -403,6 +415,93 @@ registerModel({
|
||||
return;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Key events management is performed in a Keyup to avoid intempestive RPC calls
|
||||
*
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
onKeyupTextarea(ev) {
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
}
|
||||
switch (ev.key) {
|
||||
case 'Escape':
|
||||
// already handled in _onKeydownTextarea, break to avoid default
|
||||
break;
|
||||
// ENTER, HOME, END, UP, DOWN, PAGE UP, PAGE DOWN, TAB: check if navigation in mention suggestions
|
||||
case 'Enter':
|
||||
if (this.hasSuggestions) {
|
||||
this.insertSuggestion();
|
||||
this.closeSuggestions();
|
||||
this.update({ doFocus: true });
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'PageUp':
|
||||
if (ev.key === 'ArrowUp' && !this.hasSuggestions && !this.composer.textInputContent && this.threadView) {
|
||||
this.threadView.startEditingLastMessageFromCurrentUser();
|
||||
break;
|
||||
}
|
||||
if (this.hasSuggestions) {
|
||||
this.setPreviousSuggestionActive();
|
||||
this.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 'PageDown':
|
||||
if (ev.key === 'ArrowDown' && !this.hasSuggestions && !this.composer.textInputContent && this.threadView) {
|
||||
this.threadView.startEditingLastMessageFromCurrentUser();
|
||||
break;
|
||||
}
|
||||
if (this.hasSuggestions) {
|
||||
this.setNextSuggestionActive();
|
||||
this.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
if (this.hasSuggestions) {
|
||||
this.setFirstSuggestionActive();
|
||||
this.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
if (this.hasSuggestions) {
|
||||
this.setLastSuggestionActive();
|
||||
this.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
break;
|
||||
case 'Tab':
|
||||
if (this.hasSuggestions) {
|
||||
if (ev.shiftKey) {
|
||||
this.setPreviousSuggestionActive();
|
||||
this.update({ hasToScrollToActiveSuggestion: true });
|
||||
} else {
|
||||
this.setNextSuggestionActive();
|
||||
this.update({ hasToScrollToActiveSuggestion: true });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Alt':
|
||||
case 'AltGraph':
|
||||
case 'CapsLock':
|
||||
case 'Control':
|
||||
case 'Fn':
|
||||
case 'FnLock':
|
||||
case 'Hyper':
|
||||
case 'Meta':
|
||||
case 'NumLock':
|
||||
case 'ScrollLock':
|
||||
case 'Shift':
|
||||
case 'ShiftSuper':
|
||||
case 'Symbol':
|
||||
case 'SymbolLock':
|
||||
// prevent modifier keys from resetting the suggestion state
|
||||
break;
|
||||
// Otherwise, check if a mention is typed
|
||||
default:
|
||||
this.saveStateInStore();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {ClipboardEvent} ev
|
||||
*/
|
||||
@@ -553,6 +652,17 @@ registerModel({
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Saves the composer text input state in store
|
||||
*/
|
||||
saveStateInStore() {
|
||||
this.composer.update({
|
||||
textInputContent: this.textareaRef.el.value,
|
||||
textInputCursorEnd: this.textareaRef.el.selectionEnd,
|
||||
textInputCursorStart: this.textareaRef.el.selectionStart,
|
||||
textInputSelectionDirection: this.textareaRef.el.selectionDirection,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Send a message in the composer on related thread.
|
||||
*
|
||||
@@ -1389,10 +1499,6 @@ registerModel({
|
||||
* Reference of the textarea. Useful to set height, selection and content.
|
||||
*/
|
||||
textareaRef: attr(),
|
||||
/**
|
||||
* States the OWL text input component of this composer view.
|
||||
*/
|
||||
textInputComponent: attr(),
|
||||
/**
|
||||
* States the thread view on which this composer allows editing (if any).
|
||||
*/
|
||||
|
||||
@@ -46,6 +46,21 @@ registerModel({
|
||||
closeSubtypes() {
|
||||
this.update({ followerSubtypeListDialog: clear() });
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickDetails(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.openProfile();
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickEdit(ev) {
|
||||
ev.preventDefault();
|
||||
this.showSubtypes();
|
||||
},
|
||||
/**
|
||||
* Opens the most appropriate view that is a profile for this follower.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { one } from '@mail/model/model_field';
|
||||
|
||||
registerModel({
|
||||
name: 'FollowerListMenuView',
|
||||
identifyingFields: [['chatterOwner']],
|
||||
fields: {
|
||||
chatterOwner: one('Chatter', {
|
||||
inverse: 'followerListMenuView',
|
||||
readonly: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -17,6 +17,22 @@ registerModel({
|
||||
containsElement(element) {
|
||||
return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element));
|
||||
},
|
||||
/**
|
||||
* Called when clicking on apply button.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickApply(ev) {
|
||||
this.follower.updateSubtypes();
|
||||
},
|
||||
/**
|
||||
* Called when clicking on cancel button.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickCancel(ev) {
|
||||
this.follower.closeSubtypes();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {FieldCommand}
|
||||
|
||||
@@ -6,6 +6,20 @@ import { one } from '@mail/model/model_field';
|
||||
registerModel({
|
||||
name: 'FollowerSubtypeView',
|
||||
identifyingFields: ['followerSubtypeListOwner', 'subtype'],
|
||||
recordMethods: {
|
||||
/**
|
||||
* Called when clicking on cancel button.
|
||||
*
|
||||
* @param {Event} ev
|
||||
*/
|
||||
onChangeCheckbox(ev) {
|
||||
if (ev.target.checked) {
|
||||
this.follower.selectSubtype(this.subtype);
|
||||
} else {
|
||||
this.follower.unselectSubtype(this.subtype);
|
||||
}
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
follower: one('Follower', {
|
||||
related: 'followerSubtypeListOwner.follower',
|
||||
|
||||
@@ -6,6 +6,24 @@ import { one } from '@mail/model/model_field';
|
||||
registerModel({
|
||||
name: 'MailTemplateView',
|
||||
identifyingFields: ['activityViewOwner', 'mailTemplate'],
|
||||
recordMethods: {
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickPreview(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.mailTemplate.preview(this.activityViewOwner.activity);
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickSend(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.mailTemplate.send(this.activityViewOwner.activity);
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
activityViewOwner: one('ActivityView', {
|
||||
inverse: 'mailTemplateViews',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { attr, one } from '@mail/model/model_field';
|
||||
import { clear, insertAndReplace, replace } from '@mail/model/model_field_command';
|
||||
import { markEventHandled } from '@mail/utils/utils';
|
||||
import { isEventHandled, markEventHandled } from '@mail/utils/utils';
|
||||
|
||||
registerModel({
|
||||
name: 'MessageView',
|
||||
@@ -24,6 +24,61 @@ registerModel({
|
||||
}, 2000),
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClick(ev) {
|
||||
if (ev.target.closest('.o_channel_redirect')) {
|
||||
this.messaging.openProfile({
|
||||
id: Number(ev.target.dataset.oeId),
|
||||
model: 'mail.channel',
|
||||
});
|
||||
// avoid following dummy href
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (ev.target.tagName === 'A') {
|
||||
if (ev.target.dataset.oeId && ev.target.dataset.oeModel) {
|
||||
this.messaging.openProfile({
|
||||
id: Number(ev.target.dataset.oeId),
|
||||
model: ev.target.dataset.oeModel,
|
||||
});
|
||||
// avoid following dummy href
|
||||
ev.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!isEventHandled(ev, 'Message.ClickAuthorAvatar') &&
|
||||
!isEventHandled(ev, 'Message.ClickAuthorName') &&
|
||||
!isEventHandled(ev, 'Message.ClickFailure') &&
|
||||
!isEventHandled(ev, 'MessageActionList.Click') &&
|
||||
!isEventHandled(ev, 'MessageReactionGroup.Click') &&
|
||||
!isEventHandled(ev, 'MessageInReplyToView.ClickMessageInReplyTo')
|
||||
) {
|
||||
this.update({ isClicked: !this.isClicked });
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickAuthorAvatar(ev) {
|
||||
markEventHandled(ev, 'Message.ClickAuthorAvatar');
|
||||
if (!this.hasAuthorOpenChat) {
|
||||
return;
|
||||
}
|
||||
this.message.author.openChat();
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickAuthorName(ev) {
|
||||
markEventHandled(ev, 'Message.ClickAuthorName');
|
||||
if (!this.message.author) {
|
||||
return;
|
||||
}
|
||||
this.message.author.openProfile();
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
@@ -31,6 +86,13 @@ registerModel({
|
||||
markEventHandled(ev, 'Message.ClickFailure');
|
||||
this.message.openResendAction();
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickOriginThread(ev) {
|
||||
ev.preventDefault();
|
||||
this.message.originThread.open();
|
||||
},
|
||||
onComponentUpdate() {
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
|
||||
@@ -14,6 +14,34 @@ registerModel({
|
||||
close() {
|
||||
this.update({ isOpen: false });
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickDesktopTabButton(ev) {
|
||||
this.update({ activeTabId: ev.currentTarget.dataset.tabId });
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickNewMessage(ev) {
|
||||
if (!this.messaging.device.isMobile) {
|
||||
this.messaging.chatWindowManager.openNewMessage();
|
||||
this.close();
|
||||
} else {
|
||||
this.toggleMobileNewMessage();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickToggler(ev) {
|
||||
// avoid following dummy href
|
||||
ev.preventDefault();
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
}
|
||||
this.toggleOpen();
|
||||
},
|
||||
/**
|
||||
* Toggle the visibility of the messaging menu "new message" input in
|
||||
* mobile.
|
||||
|
||||
@@ -504,6 +504,18 @@ function getCreateComposerSuggestionComponent({ env, modelManager, widget }) {
|
||||
};
|
||||
}
|
||||
|
||||
function getCreateFollowerListMenuComponent({ env, modelManager, widget }) {
|
||||
return async function createFollowerListMenuComponent(thread, props) {
|
||||
const followerListMenuView = modelManager.messaging.models['FollowerListMenuView'].create({
|
||||
qunitTest: insertAndReplace(),
|
||||
});
|
||||
return await createRootMessagingComponent(env, "FollowerListMenu", {
|
||||
props: { localId: followerListMenuView.localId, threadLocalId: thread.localId, ...props },
|
||||
target: widget.el,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getCreateMessageComponent({ env, modelManager, widget }) {
|
||||
return async function createMessageComponent(message) {
|
||||
const messageView = modelManager.messaging.models['MessageView'].create({
|
||||
@@ -930,6 +942,7 @@ async function start(param0 = {}) {
|
||||
createChatterContainerComponent: getCreateChatterContainerComponent({ afterEvent, env: testEnv, widget }),
|
||||
createComposerComponent: getCreateComposerComponent({ env: testEnv, modelManager, widget }),
|
||||
createComposerSuggestionComponent: getCreateComposerSuggestionComponent({ env: testEnv, modelManager, widget }),
|
||||
createFollowerListMenuComponent: getCreateFollowerListMenuComponent({ env: testEnv, modelManager, widget }),
|
||||
createMessageComponent: getCreateMessageComponent({ env: testEnv, modelManager, widget }),
|
||||
createMessagingMenuComponent: getCreateMessagingMenuComponent({ env: testEnv, widget }),
|
||||
createNotificationListComponent: getCreateNotificationListComponent({ env: testEnv, modelManager, widget }),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { addFields, patchIdentifyingFields } from '@mail/model/model_core';
|
||||
import { one } from '@mail/model/model_field';
|
||||
// ensure that the model definition is loaded before the patch
|
||||
import '@mail/models/follower_list_menu_view';
|
||||
|
||||
addFields('FollowerListMenuView', {
|
||||
qunitTest: one('QUnitTest', {
|
||||
inverse: 'followerListMenuView',
|
||||
readonly: true,
|
||||
}),
|
||||
});
|
||||
|
||||
patchIdentifyingFields('FollowerListMenuView', identifyingFields => {
|
||||
identifyingFields[0].push('qunitTest');
|
||||
});
|
||||
@@ -18,6 +18,10 @@ registerModel({
|
||||
inverse: 'qunitTest',
|
||||
isCausal: true,
|
||||
}),
|
||||
followerListMenuView: one('FollowerListMenuView', {
|
||||
inverse: 'qunitTest',
|
||||
isCausal: true,
|
||||
}),
|
||||
messageView: one('MessageView', {
|
||||
inverse: 'qunitTest',
|
||||
isCausal: true,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { insert, link } from '@mail/model/model_field_command';
|
||||
import {
|
||||
createRootMessagingComponent,
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
@@ -11,27 +10,17 @@ import Bus from 'web.Bus';
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
QUnit.module('components', {}, function () {
|
||||
QUnit.module('follower_list_menu_tests.js', {
|
||||
beforeEach() {
|
||||
this.createFollowerListMenuComponent = async (thread, target, otherProps = {}) => {
|
||||
const props = Object.assign({ threadLocalId: thread.localId }, otherProps);
|
||||
await createRootMessagingComponent(thread.env, "FollowerListMenu", {
|
||||
props,
|
||||
target,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
QUnit.module('follower_list_menu_tests.js');
|
||||
|
||||
QUnit.test('base rendering not editable', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const { messaging, widget } = await start();
|
||||
const { createFollowerListMenuComponent, messaging } = await start();
|
||||
const thread = messaging.models['Thread'].create({
|
||||
id: 100,
|
||||
model: 'res.partner',
|
||||
});
|
||||
await this.createFollowerListMenuComponent(thread, widget.el, { isDisabled: true });
|
||||
await createFollowerListMenuComponent(thread, { isDisabled: true });
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_FollowerListMenu',
|
||||
@@ -63,13 +52,13 @@ QUnit.test('base rendering not editable', async function (assert) {
|
||||
QUnit.test('base rendering editable', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const { click, messaging, widget } = await start();
|
||||
const { click, createFollowerListMenuComponent, messaging, widget } = await start();
|
||||
const thread = messaging.models['Thread'].create({
|
||||
id: 100,
|
||||
model: 'res.partner',
|
||||
hasWriteAccess: true,
|
||||
});
|
||||
await this.createFollowerListMenuComponent(thread, widget.el);
|
||||
await createFollowerListMenuComponent(thread, widget.el);
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -141,13 +130,13 @@ QUnit.test('click on "add followers" button', async function (assert) {
|
||||
pyEnv['res.partner'].write([payload.action.context.default_res_id], { message_follower_ids: [mailFollowerId1] });
|
||||
payload.options.on_close();
|
||||
});
|
||||
const { click, messaging, widget } = await start({ env: { bus } });
|
||||
const { click, createFollowerListMenuComponent, messaging, widget } = await start({ env: { bus } });
|
||||
const thread = messaging.models['Thread'].create({
|
||||
hasWriteAccess: true,
|
||||
id: resPartnerId1,
|
||||
model: 'res.partner',
|
||||
});
|
||||
await this.createFollowerListMenuComponent(thread, widget.el);
|
||||
await createFollowerListMenuComponent(thread, widget.el);
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -210,7 +199,7 @@ QUnit.test('click on remove follower', async function (assert) {
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv['res.partner'].create();
|
||||
const { click, messaging, widget } = await start({
|
||||
const { click, createFollowerListMenuComponent, messaging, widget } = await start({
|
||||
async mockRPC(route, args) {
|
||||
if (route.includes('message_unsubscribe')) {
|
||||
assert.step('message_unsubscribe');
|
||||
@@ -237,7 +226,7 @@ QUnit.test('click on remove follower', async function (assert) {
|
||||
name: "François Perusse",
|
||||
}),
|
||||
});
|
||||
await this.createFollowerListMenuComponent(thread, widget.el);
|
||||
await createFollowerListMenuComponent(thread, widget.el);
|
||||
|
||||
await click('.o_FollowerListMenu_buttonFollowers');
|
||||
assert.containsOnce(
|
||||
@@ -398,14 +387,14 @@ QUnit.test('Show "Add follower" and subtypes edition/removal buttons on all foll
|
||||
QUnit.test('Show "No Followers" dropdown-item if there are no followers and user dose not have write access', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const { click, messaging, widget } = await start();
|
||||
const { click, createFollowerListMenuComponent, messaging, widget } = await start();
|
||||
const thread = messaging.models['Thread'].create({
|
||||
id: 100,
|
||||
model: 'res.partner',
|
||||
hasWriteAccess: false,
|
||||
});
|
||||
|
||||
await this.createFollowerListMenuComponent(thread, widget.el);
|
||||
await createFollowerListMenuComponent(thread, widget.el);
|
||||
await click('.o_FollowerListMenu_buttonFollowers');
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
|
||||
Reference in New Issue
Block a user