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

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

Related: odoo/enterprise#26637
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-04-27 10:04:24 +02:00
parent 96288beeb6
commit ae5f59775e
12 changed files with 156 additions and 46 deletions
@@ -7,16 +7,16 @@ const { Component } = owl;
export class ChannelMemberList extends Component {
/**
* @returns {Thread}
* @returns {ChannelMemberListView}
*/
get channel() {
return this.messaging.models['Thread'].get(this.props.channelLocalId);
get channelMemberListView() {
return this.messaging && this.messaging.models['ChannelMemberListView'].get(this.props.localId);
}
}
Object.assign(ChannelMemberList, {
props: { channelLocalId: String },
props: { localId: String },
template: 'mail.ChannelMemberList',
});
@@ -2,29 +2,29 @@
<templates xml:space="preserve">
<t t-name="mail.ChannelMemberList" owl="1">
<t t-if="channel">
<t t-if="channelMemberListView">
<div class="o_ChannelMemberList d-flex flex-column overflow-auto bg-light" t-attf-class="{{ className }}" t-ref="root">
<t t-if="channel.orderedOnlineMembers.length > 0">
<t t-if="channelMemberListView.channel.orderedOnlineMembers.length > 0">
<t t-call="mail.ChannelMemberList_memberList">
<t t-set="members" t-value="channel.orderedOnlineMembers"/>
<t t-set="members" t-value="channelMemberListView.channel.orderedOnlineMembers"/>
<t t-set="title">Online</t>
</t>
</t>
<t t-if="channel.orderedOfflineMembers.length > 0">
<t t-if="channelMemberListView.channel.orderedOfflineMembers.length > 0">
<t t-call="mail.ChannelMemberList_memberList">
<t t-set="members" t-value="channel.orderedOfflineMembers"/>
<t t-set="members" t-value="channelMemberListView.channel.orderedOfflineMembers"/>
<t t-set="title">Offline</t>
</t>
</t>
<t t-if="channel.unknownMemberCount === 1">
<t t-if="channelMemberListView.channel.unknownMemberCount === 1">
<span class="mx-2 mt-2">And 1 other member.</span>
</t>
<t t-if="channel.unknownMemberCount > 1">
<span class="mx-2 mt-2">And <t t-esc="channel.unknownMemberCount"/> other members.</span>
<t t-if="channelMemberListView.channel.unknownMemberCount > 1">
<span class="mx-2 mt-2">And <t t-esc="channelMemberListView.channel.unknownMemberCount"/> other members.</span>
</t>
<t t-if="channel.unknownMemberCount > 0">
<t t-if="channelMemberListView.channel.unknownMemberCount > 0">
<div class="mx-2 my-1">
<button class="btn btn-secondary" t-on-click="channel.onClickLoadMoreMembers">Load more</button>
<button class="btn btn-secondary" t-on-click="channelMemberListView.channel.onClickLoadMoreMembers">Load more</button>
</div>
</t>
</div>
@@ -35,8 +35,8 @@
<h6 class="m-2"><t t-esc="title"/> - <t t-esc="members.length"/></h6>
<t t-foreach="members" t-as="member" t-key="member.localId">
<div class="o_ChannelMemberList_member d-flex align-items-center mx-3 my-1">
<div class="o_ChannelMemberList_avatarContainer position-relative flex-shrink-0 o_cursor_pointer" t-on-click="() => channel.onClickMemberAvatar(member)">
<img class="o_ChannelMemberList_avatar rounded-circle w-100 h-100" t-attf-src="/mail/channel/{{ channel.id }}/partner/{{ member.id }}/avatar_128" alt="Avatar"/>
<div class="o_ChannelMemberList_avatarContainer position-relative flex-shrink-0 o_cursor_pointer" t-on-click="() => channelMemberListView.channel.onClickMemberAvatar(member)">
<img class="o_ChannelMemberList_avatar rounded-circle w-100 h-100" t-attf-src="/mail/channel/{{ channelMemberListView.channel.id }}/partner/{{ member.id }}/avatar_128" alt="Avatar"/>
<t t-if="member.im_status and member.im_status !== 'im_partner'">
<PartnerImStatusIcon
@@ -49,7 +49,7 @@
/>
</t>
</div>
<span class="o_ChannelMemberList_name ml-2 flex-column-1 text-truncate o_cursor_pointer" t-on-click="() => channel.onClickMemberName(member)">
<span class="o_ChannelMemberList_name ml-2 flex-column-1 text-truncate o_cursor_pointer" t-on-click="() => channelMemberListView.channel.onClickMemberName(member)">
<t t-esc="member.nameOrDisplayName"/>
</span>
</div>
@@ -17,8 +17,8 @@
chatWindowLocalId="chatWindow.localId"
onClicked="chatWindow.onClickHeader"
/>
<t t-if="chatWindow.thread and chatWindow.thread.hasMemberListFeature and chatWindow.isMemberListOpened">
<ChannelMemberList channelLocalId="chatWindow.thread.localId" className="'bg-white'"/>
<t t-if="chatWindow.channelMemberListView">
<ChannelMemberList localId="chatWindow.channelMemberListView.localId" className="'bg-white'"/>
</t>
<t t-if="chatWindow.channelInvitationForm">
<ChannelInvitationForm className="'o_ChatWindow_channelInvitationForm'" localId="chatWindow.channelInvitationForm.localId"/>
@@ -48,7 +48,7 @@
</t>
</b>
</small>
<ComposerSuggestedRecipientList threadLocalId="composerView.composer.activeThread.localId"/>
<ComposerSuggestedRecipientList localId="composerView.composerSuggestedRecipientListView.localId" threadLocalId="composerView.composer.activeThread.localId"/>
</t>
</div>
</t>
@@ -2,24 +2,21 @@
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component, useState } = owl;
const { Component } = owl;
export class ComposerSuggestedRecipientList extends Component {
/**
* @override
*/
setup() {
super.setup();
this.state = useState({
hasShowMoreButton: false,
});
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {ComposerSuggestedRecipientListView}
*/
get composerSuggestedRecipientListView() {
return this.messaging && this.messaging.models['ComposerSuggestedRecipientListView'].get(this.props.localId);
}
/**
* @returns {Thread}
*/
@@ -31,24 +28,23 @@ export class ComposerSuggestedRecipientList extends Component {
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onClickShowLess(ev) {
this.state.hasShowMoreButton = false;
}
/**
* @private
*/
_onClickShowMore(ev) {
this.state.hasShowMoreButton = true;
if (!this.composerSuggestedRecipientListView) {
return;
}
this.composerSuggestedRecipientListView.update({ hasShowMoreButton: true });
}
}
Object.assign(ComposerSuggestedRecipientList, {
props: { threadLocalId: String },
props: {
localId: String,
threadLocalId: String,
},
template: 'mail.ComposerSuggestedRecipientList',
});
@@ -1,21 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ComposerSuggestedRecipientList" owl="1">
<t t-if="thread">
<t t-if="composerSuggestedRecipientListView and thread">
<div class="o_ComposerSuggestedRecipientList mb-2" t-attf-class="{{ className }}" t-ref="root">
<t t-foreach="state.hasShowMoreButton ? thread.suggestedRecipientInfoList : thread.suggestedRecipientInfoList.slice(0,3)" t-as="recipientInfo" t-key="recipientInfo.localId">
<t t-foreach="composerSuggestedRecipientListView.hasShowMoreButton ? thread.suggestedRecipientInfoList : thread.suggestedRecipientInfoList.slice(0,3)" t-as="recipientInfo" t-key="recipientInfo.localId">
<ComposerSuggestedRecipient
suggestedRecipientInfoLocalId="recipientInfo.localId"
/>
</t>
<t t-if="thread.suggestedRecipientInfoList.length > 3">
<t t-if="!state.hasShowMoreButton" >
<t t-if="!composerSuggestedRecipientListView.hasShowMoreButton" >
<button class="o_ComposerSuggestedRecipientList_showMore btn btn-sm btn-link" t-on-click="_onClickShowMore">
Show more
</button>
</t>
<t t-else="">
<button class="o_ComposerSuggestedRecipientList_showLess btn btn-sm btn-link" t-on-click="_onClickShowLess">
<button class="o_ComposerSuggestedRecipientList_showLess btn btn-sm btn-link" t-on-click="composerSuggestedRecipientListView.onClickShowLess">
Show less
</button>
</t>
@@ -44,8 +44,8 @@
/>
</t>
</div>
<t t-if="threadView.thread and threadView.thread.hasMemberListFeature and threadView.hasMemberList and threadView.isMemberListOpened">
<ChannelMemberList className="'o_ThreadView_channelMemberList flex-shrink-0 border-left'" channelLocalId="threadView.thread.localId"/>
<t t-if="threadView.channelMemberListView">
<ChannelMemberList className="'o_ThreadView_channelMemberList flex-shrink-0 border-left'" localId="threadView.channelMemberListView.localId"/>
</t>
</div>
</div>
@@ -0,0 +1,39 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
import { clear, replace } from '@mail/model/model_field_command';
registerModel({
name: 'ChannelMemberListView',
identifyingFields: [['chatWindowOwner', 'threadViewOwner']],
recordMethods: {
/**
* @private
* @returns {FieldCommand}
*/
_computeChannel() {
if (this.chatWindowOwner) {
return replace(this.chatWindowOwner.thread);
}
if (this.threadViewOwner) {
return replace(this.threadViewOwner.thread);
}
return clear();
},
},
fields: {
channel: one('Thread', {
compute: '_computeChannel',
readonly: true,
}),
chatWindowOwner: one('ChatWindow', {
inverse: 'channelMemberListView',
readonly: true,
}),
threadViewOwner: one('ThreadView', {
inverse: 'channelMemberListView',
readonly: true,
}),
},
});
@@ -325,6 +325,16 @@ registerModel({
this.thread.notifyFoldStateToServer('open');
}
},
/**
* @private
* @returns {FieldCommand}
*/
_computeChannelMemberListView() {
if (this.thread && this.thread.hasMemberListFeature && this.isMemberListOpened) {
return insertAndReplace();
}
return clear();
},
/**
* @private
* @returns {string}
@@ -586,6 +596,11 @@ registerModel({
inverse: 'chatWindow',
isCausal: true,
}),
channelMemberListView: one('ChannelMemberListView', {
compute: '_computeChannelMemberListView',
inverse: 'chatWindowOwner',
isCausal: true,
}),
componentStyle: attr({
compute: '_computeComponentStyle',
}),
@@ -0,0 +1,30 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
registerModel({
name: 'ComposerSuggestedRecipientListView',
identifyingFields: ['composerViewOwner'],
recordMethods: {
/**
* @param {MouseEvent} ev
*/
onClickShowLess(ev) {
if (!this.exists()) {
return;
}
this.update({ hasShowMoreButton: false });
},
},
fields: {
composerViewOwner: one('ComposerView', {
inverse: 'composerSuggestedRecipientListView',
readonly: true,
required: true,
}),
hasShowMoreButton: attr({
default: false,
}),
},
});
@@ -801,6 +801,16 @@ registerModel({
? insertAndReplace()
: clear();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeComposerSuggestedRecipientListView() {
if (this.hasHeader && this.hasFollowers && !this.composer.isLog) {
return insertAndReplace();
}
return clear();
},
/**
* @private
* @returns {FieldCommand}
@@ -1415,6 +1425,11 @@ registerModel({
inverse: 'composerViews',
required: true,
}),
composerSuggestedRecipientListView: one('ComposerSuggestedRecipientListView', {
compute: '_computeComposerSuggestedRecipientListView',
inverse: 'composerViewOwner',
isCausal: true,
}),
/**
* Current partner image URL.
*/
@@ -93,6 +93,16 @@ registerModel({
? insertAndReplace()
: clear();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeChannelMemberListView() {
if (this.thread && this.thread.hasMemberListFeature && this.hasMemberList && this.isMemberListOpened) {
return insertAndReplace();
}
return clear();
},
/**
* @private
* @returns {FieldCommand}
@@ -339,6 +349,11 @@ registerModel({
},
},
fields: {
channelMemberListView: one('ChannelMemberListView', {
compute: '_computeChannelMemberListView',
inverse: 'threadViewOwner',
isCausal: true,
}),
compact: attr({
related: 'threadViewer.compact',
}),