[IMP] mail: introduce view models (step 3)

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

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-04-29 12:05:49 +02:00
parent 90a2f5381f
commit 5bd9361c3c
22 changed files with 192 additions and 184 deletions
@@ -13,7 +13,6 @@ export class AttachmentBox extends Component {
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'AttachmentBoxView' });
this._onDropZoneFilesDropped = this._onDropZoneFilesDropped.bind(this);
}
//--------------------------------------------------------------------------
@@ -27,20 +26,6 @@ export class AttachmentBox extends Component {
return this.messaging && this.messaging.models['AttachmentBoxView'].get(this.props.localId);
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} detail
* @param {FileList} detail.files
*/
async _onDropZoneFilesDropped(detail) {
await this.attachmentBoxView.fileUploader.uploadFiles(detail.files);
this.attachmentBoxView.useDragVisibleDropZone.update({ isVisible: false });
}
}
Object.assign(AttachmentBox, {
@@ -16,7 +16,6 @@
<DropZone
className="'o_AttachmentBox_dropZone'"
localId="attachmentBoxView.dropZoneView.localId"
onDropzoneFilesDropped="_onDropZoneFilesDropped"
/>
</t>
<t t-if="attachmentBoxView.attachmentList">
@@ -33,7 +33,6 @@
<t t-if="chatter.threadView">
<ThreadView
className="'o_Chatter_thread'"
hasScrollAdjust="chatter.hasMessageListScrollAdjust"
localId="chatter.threadView.localId"
/>
</t>
@@ -15,7 +15,6 @@ export class Composer extends Component {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'ComposerView' });
useRefToModel({ fieldName: 'buttonEmojisRef', modelName: 'ComposerView', refName: 'buttonEmojis' });
this._onDropZoneFilesDropped = this._onDropZoneFilesDropped.bind(this);
}
//--------------------------------------------------------------------------
@@ -29,22 +28,6 @@ export class Composer extends Component {
return this.messaging && this.messaging.models['ComposerView'].get(this.props.localId);
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when some files have been dropped in the dropzone.
*
* @private
* @param {Object} detail
* @param {FileList} detail.files
*/
async _onDropZoneFilesDropped(detail) {
await this.composerView.fileUploader.uploadFiles(detail.files);
this.composerView.useDragVisibleDropZone.update({ isVisible: false });
}
}
Object.assign(Composer, {
@@ -21,7 +21,6 @@
<DropZone
className="'o_Composer_dropZone'"
localId="composerView.dropZoneView.localId"
onDropzoneFilesDropped="_onDropZoneFilesDropped"
/>
</t>
<t t-if="composerView.hasHeader">
@@ -4,13 +4,7 @@
<t t-name="mail.DiscussMobileMailboxSelection" owl="1">
<div class="o_DiscussMobileMailboxSelection d-flex" t-attf-class="{{ className }}" t-ref="root">
<t t-foreach="discussView.discuss.orderedMailboxes" t-as="mailbox" t-key="mailbox.localId">
<button class="o_DiscussMobileMailboxSelection_button btn btn-secondary flex-grow-1 p-2"
t-att-class="{
'active o-active shadow-none': discussView.discuss.thread === mailbox,
}" t-on-click="() => discussView.onClickMobileMailboxSelectionItem(mailbox)" t-att-data-mailbox-local-id="mailbox.localId" type="button"
>
<t t-esc="mailbox.name"/>
</button>
<DiscussMobileMailboxSelectionItem discussViewLocalId="discussView.localId" mailboxLocalId="mailbox.localId"/>
</t>
</div>
</t>
@@ -0,0 +1,37 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class DiscussMobileMailboxSelectionItem extends Component {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {DiscussView}
*/
get discussView() {
return this.messaging && this.messaging.models['DiscussView'].get(this.props.discussViewLocalId);
}
/**
* @returns {Thread}
*/
get mailbox() {
return this.messaging && this.messaging.models['Thread'].get(this.props.mailboxLocalId);
}
}
Object.assign(DiscussMobileMailboxSelectionItem, {
props: {
discussViewLocalId: String,
mailboxLocalId: String,
},
template: 'mail.DiscussMobileMailboxSelectionItem',
});
registerMessagingComponent(DiscussMobileMailboxSelectionItem);
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.DiscussMobileMailboxSelectionItem" owl="1">
<button class="o_DiscussMobileMailboxSelectionItem btn btn-secondary flex-grow-1 p-2"
t-att-class="{
'active o-active shadow-none': discussView.discuss.thread === mailbox,
}" t-attf-class="{{ className }}" t-on-click="() => discussView.onClickMobileMailboxSelectionItem(mailbox)" t-att-data-mailbox-local-id="mailbox.localId" type="button" t-ref="root"
>
<t t-esc="mailbox.name"/>
</button>
</t>
</templates>
@@ -2,29 +2,10 @@
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component, useState } = owl;
const { Component } = owl;
export class DropZone extends Component {
/**
* @override
*/
setup() {
super.setup();
this.state = useState({
/**
* Determine whether the user is dragging files over the dropzone.
* Useful to provide visual feedback in that case.
*/
isDraggingInside: false,
});
/**
* Counts how many drag enter/leave happened on self and children. This
* ensures the drop effect stays active when dragging over a child.
*/
this._dragCount = 0;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -46,94 +27,6 @@ export class DropZone extends Component {
return Boolean(this.root.el && this.root.el.contains(node));
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Making sure that dragging content is external files.
* Ignoring other content dragging like text.
*
* @private
* @param {DataTransfer} dataTransfer
* @returns {boolean}
*/
_isDragSourceExternalFile(dataTransfer) {
const dragDataType = dataTransfer.types;
if (dragDataType.constructor === window.DOMStringList) {
return dragDataType.contains('Files');
}
if (dragDataType.constructor === Array) {
return dragDataType.includes('Files');
}
return false;
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Shows a visual drop effect when dragging inside the dropzone.
*
* @private
* @param {DragEvent} ev
*/
_onDragenter(ev) {
ev.preventDefault();
if (this._dragCount === 0) {
this.state.isDraggingInside = true;
}
this._dragCount++;
}
/**
* Hides the visual drop effect when dragging outside the dropzone.
*
* @private
* @param {DragEvent} ev
*/
_onDragleave(ev) {
this._dragCount--;
if (this._dragCount === 0) {
this.state.isDraggingInside = false;
}
}
/**
* Prevents default (from the template) in order to receive the drop event.
* The drop effect cursor works only when set on dragover.
*
* @private
* @param {DragEvent} ev
*/
_onDragover(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = 'copy';
}
/**
* Trigger callback 'props.onDropzoneFilesDropped' with event when new files are dropped
* on the dropzone, and then removes the visual drop effect.
*
* The parents should handle this event to process the files as they wish,
* such as uploading them.
*
* @private
* @param {DragEvent} ev
*/
_onDrop(ev) {
ev.preventDefault();
if (this._isDragSourceExternalFile(ev.dataTransfer)) {
if (this.props.onDropzoneFilesDropped) {
this.props.onDropzoneFilesDropped({
files: ev.dataTransfer.files,
});
}
}
this.state.isDraggingInside = false;
}
}
Object.assign(DropZone, {
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="mail.DropZone" owl="1">
<div class="o_DropZone position-absolute top-0 start-0 align-items-center justify-content-center d-flex w-100 h-100 border-primary bg-white-75 text-primary" t-att-class="{ 'o-dragging-inside': state.isDraggingInside }" t-attf-class="{{ className }}" t-on-dragenter="_onDragenter" t-on-dragleave="_onDragleave" t-on-dragover="_onDragover" t-on-drop="_onDrop" t-ref="root">
<div class="o_DropZone position-absolute top-0 start-0 align-items-center justify-content-center d-flex w-100 h-100 border-primary bg-white-75 text-primary" t-att-class="{ 'o-dragging-inside': dropZoneView.isDraggingInside }" t-attf-class="{{ className }}" t-on-dragenter="dropZoneView.onDragenter" t-on-dragleave="dropZoneView.onDragleave" t-on-dragover="dropZoneView.onDragover" t-on-drop="dropZoneView.onDrop" t-ref="root">
<h4>
Drag Files Here <i class="fa fa-download"/>
</h4>
@@ -13,7 +13,6 @@ export class MessageActionList extends Component {
setup() {
super.setup();
useRefToModel({ fieldName: 'actionReactionRef', modelName: 'MessageActionList', refName: 'actionReaction' });
this.ADD_A_REACTION = this.env._t("Add a Reaction");
}
/**
@@ -3,7 +3,7 @@
<t t-name="mail.MessageActionList" owl="1">
<t t-if="messageActionList">
<div class="o_MessageActionList d-flex border rounded-sm bg-white" t-attf-class="{{ className }}" t-on-click="messageActionList.onClick" t-ref="root">
<i t-if="messageActionList.message.hasReactionIcon" class="o_MessageActionList_action o_MessageActionList_actionReaction fa fa-lg fa-smile-o p-2 o_cursor_pointer" t-att-title="ADD_A_REACTION" t-on-click="messageActionList.onClickActionReaction" t-ref="actionReaction"/>
<i t-if="messageActionList.message.hasReactionIcon" class="o_MessageActionList_action o_MessageActionList_actionReaction fa fa-lg fa-smile-o p-2 o_cursor_pointer" t-att-title="messageActionList.addReactionText" t-on-click="messageActionList.onClickActionReaction" t-ref="actionReaction"/>
<PopoverView t-if="messageActionList.reactionPopoverView" localId="messageActionList.reactionPopoverView.localId"/>
<span t-if="messageActionList.message.canStarBeToggled" class="o_MessageActionList_action o_MessageActionList_actionStar p-2 o_cursor_pointer" t-att-class="{
'o_MessageActionList_actionStar_active': messageActionList.message.isStarred,
@@ -39,7 +39,6 @@ export class MessageList extends Component {
return {
componentHintList: threadView ? [...threadView.componentHintList] : [],
hasAutoScrollOnMessageReceived: threadView && threadView.hasAutoScrollOnMessageReceived,
hasScrollAdjust: this.props.hasScrollAdjust,
messageListView,
order: threadView && threadView.order,
orderedMessages: threadCache ? [...threadCache.orderedMessages] : [],
@@ -160,11 +159,10 @@ export class MessageList extends Component {
_adjustScrollForExtraMessagesAtTheEnd() {
const {
hasAutoScrollOnMessageReceived,
hasScrollAdjust,
messageListView,
order,
} = this._lastRenderedValues();
if (!messageListView.getScrollableElement() || !hasScrollAdjust) {
if (!messageListView.getScrollableElement() || !messageListView.hasScrollAdjust) {
return;
}
if (!hasAutoScrollOnMessageReceived) {
@@ -182,13 +180,12 @@ export class MessageList extends Component {
*/
_adjustScrollForExtraMessagesAtTheStart() {
const {
hasScrollAdjust,
messageListView,
order,
} = this._lastRenderedValues();
if (
!messageListView.getScrollableElement() ||
!hasScrollAdjust ||
!messageListView.hasScrollAdjust ||
!this._willPatchSnapshot ||
order === 'desc'
) {
@@ -203,12 +200,11 @@ export class MessageList extends Component {
*/
_adjustScrollFromModel() {
const {
hasScrollAdjust,
messageListView,
threadCacheInitialScrollHeight,
threadCacheInitialScrollPosition,
} = this._lastRenderedValues();
if (!messageListView.getScrollableElement() || !hasScrollAdjust) {
if (!messageListView.getScrollableElement() || !messageListView.hasScrollAdjust) {
return;
}
if (
@@ -360,16 +356,7 @@ export class MessageList extends Component {
Object.assign(MessageList, {
components: { Transition },
defaultProps: {
hasScrollAdjust: true,
},
props: {
hasScrollAdjust: {
type: Boolean,
optional: true,
},
localId: String,
},
props: { localId: String },
template: 'mail.MessageList',
});
@@ -1,5 +1,6 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component, onMounted, onWillUnmount } = owl;
@@ -11,6 +12,7 @@ export class MessagingMenu extends Component {
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'MessagingMenu' });
/**
* global JS generated ID for this component. Useful to provide a
* custom class to autocomplete input, so that click in an autocomplete
@@ -20,13 +20,7 @@ export class ThreadView extends Component {
}
Object.assign(ThreadView, {
props: {
hasScrollAdjust: {
type: Boolean,
optional: true,
},
localId: String,
},
props: { localId: String },
template: 'mail.ThreadView',
});
@@ -20,7 +20,6 @@
<t t-elif="threadView.messageListView">
<MessageList
className="'o_ThreadView_messageList'"
hasScrollAdjust="props.hasScrollAdjust"
localId="threadView.messageListView.localId"
/>
</t>
@@ -1,11 +1,95 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
import { attr, one } from '@mail/model/model_field';
import { decrement, increment } from '@mail/model/model_field_command';
registerModel({
name: 'DropZoneView',
identifyingFields: [['attachmentBoxViewOwner', 'composerViewOwner']],
recordMethods: {
/**
* Shows a visual drop effect when dragging inside the dropzone.
*
* @param {DragEvent} ev
*/
onDragenter(ev) {
if (!this.exists()) {
return;
}
ev.preventDefault();
if (this.dragCount === 0) {
this.update({ isDraggingInside: true });
}
this.update({ dragCount: increment() });
},
/**
* Hides the visual drop effect when dragging outside the dropzone.
*
* @param {DragEvent} ev
*/
onDragleave(ev) {
if (!this.exists()) {
return;
}
this.update({ dragCount: decrement() });
if (this.dragCount === 0) {
this.update({ isDraggingInside: false });
}
},
/**
* Prevents default (from the template) in order to receive the drop event.
* The drop effect cursor works only when set on dragover.
*
* @param {DragEvent} ev
*/
onDragover(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = 'copy';
},
/**
* Trigger callback 'props.onDropzoneFilesDropped' with event when new files are dropped
* on the dropzone, and then removes the visual drop effect.
*
* The parents should handle this event to process the files as they wish,
* such as uploading them.
*
* @param {DragEvent} ev
*/
async onDrop(ev) {
if (!this.exists()) {
return;
}
ev.preventDefault();
this.update({ isDraggingInside: false });
if (this._isDragSourceExternalFile(ev.dataTransfer)) {
if (this.attachmentBoxViewOwner) {
await this.attachmentBoxViewOwner.fileUploader.uploadFiles(ev.dataTransfer.files);
}
if (this.composerViewOwner) {
await this.composerViewOwner.fileUploader.uploadFiles(ev.dataTransfer.files);
}
}
},
/**
* Making sure that dragging content is external files.
* Ignoring other content dragging like text.
*
* @private
* @param {DataTransfer} dataTransfer
* @returns {boolean}
*/
_isDragSourceExternalFile(dataTransfer) {
const dragDataType = dataTransfer.types;
if (dragDataType.constructor === window.DOMStringList) {
return dragDataType.contains('Files');
}
if (dragDataType.constructor === Array) {
return dragDataType.includes('Files');
}
return false;
},
},
fields: {
attachmentBoxViewOwner: one('AttachmentBoxView', {
inverse: 'dropZoneView',
@@ -15,5 +99,19 @@ registerModel({
inverse: 'dropZoneView',
readonly: true,
}),
/**
* Counts how many drag enter/leave happened on self and children. This
* ensures the drop effect stays active when dragging over a child.
*/
dragCount: attr({
default: 0,
}),
/**
* Determines whether the user is dragging files over the dropzone.
* Useful to provide visual feedback in that case.
*/
isDraggingInside: attr({
default: false,
}),
},
});
@@ -72,6 +72,13 @@ registerModel({
onClickToggleStar(ev) {
this.message.toggleStar();
},
/**
* @private
* @returns {string}
*/
_computeAddReactionText() {
return this.env._t("Add a Reaction");
},
/**
* @private
* @returns {boolean}
@@ -103,6 +110,9 @@ registerModel({
* States the reference to the reaction action in the component.
*/
actionReactionRef: attr(),
addReactionText: attr({
compute: '_computeAddReactionText',
}),
deleteConfirmDialog: one('Dialog', {
inverse: 'messageActionListOwnerAsDeleteConfirm',
isCausal: true,
@@ -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: 'MessageListView',
@@ -16,6 +17,16 @@ registerModel({
}
return this.component.root.el;
},
/**
* @private
* @returns {boolean|FieldCommand}
*/
_computeHasScrollAdjust() {
if (this.threadViewOwner.threadViewer.chatter) {
return this.threadViewOwner.threadViewer.chatter.hasMessageListScrollAdjust;
}
return clear();
},
/***
* @private
* @returns {boolean}
@@ -38,6 +49,10 @@ registerModel({
* States the OWL component of this message list view
*/
component: attr(),
hasScrollAdjust: attr({
compute: '_computeHasScrollAdjust',
default: true,
}),
/**
* States whether the message list scroll position is at the end of
* the message list. Depending of the message list order, this could be
@@ -122,6 +122,7 @@ registerModel({
activeTabId: attr({
default: 'all',
}),
component: attr(),
/**
* States the counter of this messaging menu. The counter is an integer
* value to give to the current user an estimate of how many things
@@ -11,7 +11,7 @@ registerModel({
_created() {
document.addEventListener('dragenter', this._onDragenterListener, true);
document.addEventListener('dragleave', this._onDragleaveListener, true);
document.addEventListener('drop', this._onDropListener, true);
document.addEventListener('drop', this._onDropListener);
// Thoses Events prevent the browser to open or download the file if
// it's dropped outside of the dropzone
@@ -21,7 +21,7 @@ registerModel({
_willDelete() {
document.removeEventListener('dragenter', this._onDragenterListener, true);
document.removeEventListener('dragleave', this._onDragleaveListener, true);
document.removeEventListener('drop', this._onDropListener, true);
document.removeEventListener('drop', this._onDropListener);
window.removeEventListener('dragover', ev => ev.preventDefault());
window.removeEventListener('drop', ev => ev.preventDefault());
@@ -48,13 +48,13 @@ QUnit.test('select another mailbox', async function (assert) {
);
assert.containsOnce(
document.body,
`.o_DiscussMobileMailboxSelection_button[
`.o_DiscussMobileMailboxSelectionItem[
data-mailbox-local-id="${messaging.starred.localId}"
]`,
"should have a button to open starred mailbox"
);
await click(`.o_DiscussMobileMailboxSelection_button[
await click(`.o_DiscussMobileMailboxSelectionItem[
data-mailbox-local-id="${messaging.starred.localId}"]
`);
assert.containsOnce(
@@ -106,7 +106,7 @@ QUnit.test('auto-select "Inbox" when discuss had channel as active thread', asyn
"'mailbox' tab should be selected after click on mailbox tab"
);
assert.hasClass(
document.querySelector(`.o_DiscussMobileMailboxSelection_button[data-mailbox-local-id="${
document.querySelector(`.o_DiscussMobileMailboxSelectionItem[data-mailbox-local-id="${
messaging.inbox.localId
}"]`),
'o-active',