[IMP] mail: move component handlers to models (step 3)

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#88594

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-04-13 11:55:42 +02:00
parent e9ea0692bb
commit df31b7d9ba
11 changed files with 87 additions and 141 deletions
@@ -22,7 +22,6 @@ export class ChatWindow extends Component {
// the following are passed as props to children
this._onAutocompleteSelect = this._onAutocompleteSelect.bind(this);
this._onAutocompleteSource = this._onAutocompleteSource.bind(this);
this._onClickedHeader = this._onClickedHeader.bind(this);
this._onFocusinThread = this._onFocusinThread.bind(this);
}
@@ -111,25 +110,6 @@ export class ChatWindow extends Component {
});
}
/**
* Called when clicking on header of chat window. Usually folds the chat
* window.
*
* @private
*/
_onClickedHeader() {
if (!this.chatWindow || this.messaging.device.isMobile) {
return;
}
if (this.chatWindow.isFolded) {
this.chatWindow.unfold();
this.chatWindow.focus();
} else {
this.chatWindow.saveThreadScrollTop();
this.chatWindow.fold();
}
}
/**
* Called when an element in the thread becomes focused.
*
@@ -17,7 +17,7 @@
chatWindowLocalId="chatWindow.localId"
hasCloseAsBackButton="props.hasCloseAsBackButton"
isExpandable="props.isExpandable"
onClicked="_onClickedHeader"
onClicked="chatWindow.onClickHeader"
/>
<t t-if="chatWindow.thread and chatWindow.thread.hasMemberListFeature and chatWindow.isMemberListOpened">
<ChannelMemberList channelLocalId="chatWindow.thread.localId" className="'bg-white'"/>
@@ -18,8 +18,6 @@ export class Composer extends Component {
useComponentToModel({ fieldName: 'component', modelName: 'ComposerView' });
useRefToModel({ fieldName: 'buttonEmojisRef', modelName: 'ComposerView', refName: 'buttonEmojis' });
this._onDropZoneFilesDropped = this._onDropZoneFilesDropped.bind(this);
this._onComposerTextInputSendShortcut = this._onComposerTextInputSendShortcut.bind(this);
this._onPasteTextInput = this._onPasteTextInput.bind(this);
}
//--------------------------------------------------------------------------
@@ -37,33 +35,6 @@ export class Composer extends Component {
// Handlers
//--------------------------------------------------------------------------
/**
* Called when clicking on "discard" button.
*
* @private
* @param {MouseEvent} ev
*/
_onClickDiscard(ev) {
this.composerView.discard();
}
/**
* Called when clicking on "send" button.
*
* @private
*/
_onClickSend() {
this.composerView.sendMessage();
this.composerView.update({ doFocus: true });
}
/**
* @private
*/
_onComposerTextInputSendShortcut() {
this.composerView.sendMessage();
}
/**
* Called when some files have been dropped in the dropzone.
*
@@ -76,17 +47,6 @@ export class Composer extends Component {
this.isDropZoneVisible.value = false;
}
/**
* @private
* @param {CustomEvent} ev
*/
async _onPasteTextInput(ev) {
if (!ev.clipboardData || !ev.clipboardData.files) {
return;
}
await this.composerView.fileUploader.uploadFiles(ev.clipboardData.files);
}
}
Object.assign(Composer, {
@@ -77,8 +77,6 @@
}"
localId="composerView.localId"
hasMentionSuggestionsBelowPosition="props.hasMentionSuggestionsBelowPosition"
onComposerTextInputSendShortcut="_onComposerTextInputSendShortcut"
onPaste="_onPasteTextInput"
t-key="composerView.localId"
/>
<div class="o_Composer_buttons" t-att-class="{ 'o-composer-is-compact': composerView.isCompact, 'o-mobile': messaging.device.isMobile, 'o-messaging-in-editing': composerView and composerView.messageViewInEditing }">
@@ -150,14 +148,14 @@
}"
t-att-disabled="!composerView.composer.canPostMessage ? 'disabled' : ''"
type="button"
t-on-click="_onClickSend"
t-on-click="composerView.onClickSend"
>
<t t-if="!messaging.device.isMobile"><t t-esc="composerView.sendButtonText"/></t>
<t t-else=""><i class="fa fa-paper-plane-o"/></t>
</button>
</t>
<t t-if="!messaging.device.isMobile and props.hasDiscardButton">
<button class="o_Composer_actionButton o-last o_Composer_button o_Composer_buttonDiscard btn btn-secondary" t-att-class="{ 'o-composer-is-compact': composerView.isCompact, 'o-has-current-partner-avatar': props.hasCurrentPartnerAvatar }" type="button" t-on-click="_onClickDiscard">
<button class="o_Composer_actionButton o-last o_Composer_button o_Composer_buttonDiscard btn btn-secondary" t-att-class="{ 'o-composer-is-compact': composerView.isCompact, 'o-has-current-partner-avatar': props.hasCurrentPartnerAvatar }" type="button" t-on-click="composerView.onClickDiscard">
Discard
</button>
</t>
@@ -48,19 +48,6 @@ export class ComposerSuggestion extends Component {
}
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Event} ev
*/
_onClick(ev) {
ev.preventDefault();
this.composerSuggestion.composerViewOwner.onClickSuggestion(this.composerSuggestion);
}
}
Object.assign(ComposerSuggestion, {
@@ -3,7 +3,7 @@
<t t-name="mail.ComposerSuggestion" owl="1">
<t t-if="composerSuggestion">
<a class="o_ComposerSuggestion dropdown-item d-flex w-100 py-2 px-4" t-att-class="{ 'active bg-300': props.isActive }" t-attf-class="{{ className }}" href="#" t-att-title="composerSuggestion.title" role="menuitem" t-on-click="_onClick" t-ref="root">
<a class="o_ComposerSuggestion dropdown-item d-flex w-100 py-2 px-4" t-att-class="{ 'active bg-300': props.isActive }" t-attf-class="{{ className }}" href="#" t-att-title="composerSuggestion.title" role="menuitem" t-on-click="composerSuggestion.onClick" t-ref="root">
<t t-if="composerSuggestion.cannedResponse">
<strong class="o_ComposerSuggestion_part1 flex-shrink-0 mw-100 pr-2 text-truncate"><t t-esc="composerSuggestion.record.source"/></strong>
<em class="o_ComposerSuggestion_part2 text-600 text-truncate"><t t-esc="composerSuggestion.record.substitution"/></em>
@@ -1,11 +1,12 @@
/** @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';
import { markEventHandled } from '@mail/utils/utils';
const { Component, useRef } = owl;
const { Component } = owl;
export class ComposerTextInput extends Component {
@@ -15,6 +16,8 @@ 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' });
/**
* Updates the composer text input content when composer is mounted
* as textarea content can't be changed from the DOM.
@@ -25,16 +28,6 @@ export class ComposerTextInput extends Component {
* whether the current partner is typing something.
*/
this._textareaLastInputValue = "";
/**
* Reference of the textarea. Useful to set height, selection and content.
*/
this._textareaRef = useRef('textarea');
/**
* This is the invisible textarea used to compute the composer height
* based on the text content. We need it to downsize the textarea
* properly without flicker.
*/
this._mirroredTextareaRef = useRef('mirroredTextarea');
}
//--------------------------------------------------------------------------
@@ -55,8 +48,8 @@ export class ComposerTextInput extends Component {
this.composerView.composer.update({
textInputContent: this._getContent(),
textInputCursorEnd: this._getSelectionEnd(),
textInputCursorStart: this._getSelectionStart(),
textInputSelectionDirection: this._textareaRef.el.selectionDirection,
textInputCursorStart: this.composerView.textareaRef.el.selectionStart,
textInputSelectionDirection: this.composerView.textareaRef.el.selectionDirection,
});
}
@@ -71,7 +64,7 @@ export class ComposerTextInput extends Component {
* @returns {string}
*/
_getContent() {
return this._textareaRef.el.value;
return this.composerView.textareaRef.el.value;
}
/**
@@ -81,18 +74,7 @@ export class ComposerTextInput extends Component {
* @returns {integer}
*/
_getSelectionEnd() {
return this._textareaRef.el.selectionEnd;
}
/**
* Returns selection start position.
*
* @private
* @returns {integer}
*
*/
_getSelectionStart() {
return this._textareaRef.el.selectionStart;
return this.composerView.textareaRef.el.selectionEnd;
}
/**
@@ -122,12 +104,12 @@ export class ComposerTextInput extends Component {
if (this.messaging.device.isMobile) {
this.root.el.scrollIntoView();
}
this._textareaRef.el.focus();
this.composerView.textareaRef.el.focus();
}
if (this.composerView.hasToRestoreContent) {
this._textareaRef.el.value = this.composerView.composer.textInputContent;
this.composerView.textareaRef.el.value = this.composerView.composer.textInputContent;
if (this.composerView.isFocused) {
this._textareaRef.el.setSelectionRange(
this.composerView.textareaRef.el.setSelectionRange(
this.composerView.composer.textInputCursorStart,
this.composerView.composer.textInputCursorEnd,
this.composerView.composer.textInputSelectionDirection,
@@ -144,8 +126,8 @@ export class ComposerTextInput extends Component {
* @private
*/
_updateHeight() {
this._mirroredTextareaRef.el.value = this.composerView.composer.textInputContent;
this._textareaRef.el.style.height = (this._mirroredTextareaRef.el.scrollHeight) + "px";
this.composerView.mirroredTextareaRef.el.value = this.composerView.composer.textInputContent;
this.composerView.textareaRef.el.style.height = this.composerView.mirroredTextareaRef.el.scrollHeight + 'px';
}
//--------------------------------------------------------------------------
@@ -163,16 +145,6 @@ export class ComposerTextInput extends Component {
this.saveStateInStore();
}
/**
* @private
*/
_onFocusinTextarea() {
if (!this.composerView) {
return;
}
this.composerView.update({ isFocused: true });
}
/**
* @private
*/
@@ -192,10 +164,10 @@ export class ComposerTextInput extends Component {
return;
}
this.saveStateInStore();
if (this._textareaLastInputValue !== this._textareaRef.el.value) {
if (this._textareaLastInputValue !== this.composerView.textareaRef.el.value) {
this.composerView.handleCurrentPartnerIsTyping();
}
this._textareaLastInputValue = this._textareaRef.el.value;
this._textareaLastInputValue = this.composerView.textareaRef.el.value;
this._updateHeight();
}
@@ -254,9 +226,7 @@ export class ComposerTextInput extends Component {
!ev.metaKey &&
!ev.shiftKey
) {
if (this.props.onComposerTextInputSendShortcut) {
this.props.onComposerTextInputSendShortcut();
}
this.composerView.sendMessage();
ev.preventDefault();
return;
}
@@ -267,9 +237,7 @@ export class ComposerTextInput extends Component {
!ev.metaKey &&
!ev.shiftKey
) {
if (this.props.onComposerTextInputSendShortcut) {
this.props.onComposerTextInputSendShortcut();
}
this.composerView.sendMessage();
ev.preventDefault();
return;
}
@@ -280,9 +248,7 @@ export class ComposerTextInput extends Component {
ev.metaKey &&
!ev.shiftKey
) {
if (this.props.onComposerTextInputSendShortcut) {
this.props.onComposerTextInputSendShortcut();
}
this.composerView.sendMessage();
ev.preventDefault();
return;
}
@@ -389,14 +355,6 @@ Object.assign(ComposerTextInput, {
type: Boolean,
optional: true,
},
onComposerTextInputSendShortcut: {
type: Function,
optional: true,
},
onPaste: {
type: Function,
optional: true,
},
},
template: 'mail.ComposerTextInput',
});
@@ -3,14 +3,14 @@
<t t-name="mail.ComposerTextInput" owl="1">
<t t-if="composerView">
<div class="o_ComposerTextInput" t-attf-class="{{ className }}" t-on-paste="props.onPaste" t-ref="root">
<div class="o_ComposerTextInput" t-attf-class="{{ className }}" t-on-paste="composerView.onPasteTextInput" t-ref="root">
<t t-if="composerView.hasSuggestions">
<ComposerSuggestionList
composerViewLocalId="composerView.localId"
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="_onFocusinTextarea" t-on-focusout="_onFocusoutTextarea" t-on-keydown="_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="_onClickTextarea" t-on-focusin="composerView.onFocusinTextarea" t-on-focusout="_onFocusoutTextarea" t-on-keydown="_onKeydownTextarea" t-on-keyup="_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
@@ -135,6 +135,22 @@ registerModel({
ev.stopPropagation();
this.expand();
},
/**
* Called when clicking on header of chat window. Usually folds the chat
* window.
*/
onClickHeader(ev) {
if (!this.exists() || this.messaging.device.isMobile) {
return;
}
if (this.isFolded) {
this.unfold();
this.focus();
} else {
this.saveThreadScrollTop();
this.fold();
}
},
/**
* Handles click on the "stop adding users" button.
*
@@ -14,6 +14,13 @@ registerModel({
name: 'ComposerSuggestion',
identifyingFields: [['composerViewOwnerAsExtraSuggestion', 'composerViewOwnerAsMainSuggestion'], ['cannedResponse', 'channelCommand', 'partner', 'thread']],
recordMethods: {
/**
* @param {Event} ev
*/
onClick(ev) {
ev.preventDefault();
this.composerViewOwner.onClickSuggestion(this);
},
/**
* @private
* @returns {FieldCommand}
@@ -214,6 +214,14 @@ registerModel({
}
this.discard();
},
/**
* Called when clicking on "discard" button.
*
* @param {MouseEvent} ev
*/
onClickDiscard(ev) {
this.discard();
},
/**
* @private
* @param {MouseEvent} ev
@@ -256,6 +264,13 @@ registerModel({
}
this.postMessage();
},
/**
* Called when clicking on "send" button.
*/
onClickSend() {
this.sendMessage();
this.update({ doFocus: true });
},
/**
* Handles click on the "stop replying" button.
*
@@ -275,6 +290,12 @@ registerModel({
this.closeSuggestions();
this.update({ doFocus: true });
},
onFocusinTextarea() {
if (!this.exists()) {
return;
}
this.update({ isFocused: true });
},
/**
* @param {KeyboardEvent} ev
*/
@@ -303,6 +324,15 @@ registerModel({
markEventHandled(ev, 'Composer.closeEmojisPopover');
}
},
/**
* @param {ClipboardEvent} ev
*/
async onPasteTextInput(ev) {
if (!ev.clipboardData || !ev.clipboardData.files) {
return;
}
await this.fileUploader.uploadFiles(ev.clipboardData.files);
},
/**
* Open the full composer modal.
*/
@@ -1225,6 +1255,12 @@ registerModel({
inverse: 'composerViewInEditing',
readonly: true,
}),
/**
* This is the invisible textarea used to compute the composer height
* based on the text content. We need it to downsize the textarea
* properly without flicker.
*/
mirroredTextareaRef: attr(),
/**
* Determines the label on the send button of this composer view.
*/
@@ -1270,6 +1306,10 @@ registerModel({
suggestionSearchTerm: attr({
compute: '_computeSuggestionSearchTerm',
}),
/**
* Reference of the textarea. Useful to set height, selection and content.
*/
textareaRef: attr(),
/**
* States the OWL text input component of this composer view.
*/