[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:
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user