[IMP] mail: introduce and clean follower views

Part of task-2810734

closes odoo/odoo#92193

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2022-05-24 19:11:06 +02:00
parent 677eee90a9
commit f2877f711a
10 changed files with 168 additions and 159 deletions
@@ -62,8 +62,6 @@
<FollowerListMenu
className="'o_ChatterTopbar_followerListMenu'"
record="chatterTopbar.chatter.followerListMenuView"
thread="chatterTopbar.chatter.thread"
isChatterButton="true"
/>
</t>
</div>
@@ -5,31 +5,16 @@ import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class Follower extends Component {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {Follower}
* @returns {FollowerView}
*/
get follower() {
return this.props.follower;
get followerView() {
return this.props.record;
}
}
Object.assign(Follower, {
defaultProps: {
onClick: () => {},
},
props: {
follower: Object,
onClick: {
type: Function,
optional: true,
},
},
props: { record: Object },
template: 'mail.Follower',
});
@@ -2,17 +2,17 @@
<templates xml:space="preserve">
<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="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"/>
<t t-if="followerView">
<div class="o_Follower d-flex justify-content-between p-0" t-attf-class="{{ className }}" t-on-click="followerView.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': !followerView.follower.isActive }" href="#" t-on-click="followerView.onClickDetails">
<img class="o_Follower_avatar me-2 rounded-circle" t-att-src="followerView.follower.partner.avatarUrl" alt="Avatar"/>
<span class="o_Follower_name flex-shrink text-truncate" t-esc="followerView.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="follower.onClickEdit">
<t t-if="followerView.follower.isEditable">
<button class="o_Follower_button o_Follower_editButton btn btn-icon rounded-0" title="Edit subscription" t-on-click="followerView.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="follower.onClickRemove">
<button class="o_Follower_button o_Follower_removeButton btn btn-icon rounded-0" title="Remove this follower" t-on-click="followerView.onClickRemove">
<i class="fa fa-remove"/>
</button>
</t>
@@ -36,13 +36,6 @@ export class FollowerListMenu extends Component {
return this.props.record;
}
/**
* @return {Thread}
*/
get thread() {
return this.props.thread;
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -54,7 +47,7 @@ export class FollowerListMenu extends Component {
_onClickAddFollowers(ev) {
ev.preventDefault();
this.followerListMenuView.hide();
this.thread.promptAddPartnerFollower();
this.followerListMenuView.chatterOwner.thread.promptAddPartnerFollower();
}
/**
@@ -73,14 +66,7 @@ export class FollowerListMenu extends Component {
}
Object.assign(FollowerListMenu, {
defaultProps: {
isChatterButton: false,
},
props: {
thread: Object,
isChatterButton: { type: Boolean, optional: true },
record: Object,
},
props: { record: Object },
template: 'mail.FollowerListMenu',
});
@@ -2,33 +2,32 @@
<templates xml:space="preserve">
<t t-name="mail.FollowerListMenu" owl="1">
<t t-if="followerListMenuView and thread">
<t t-if="followerListMenuView">
<div class="o_FollowerListMenu position-relative d-flex" t-attf-class="{{ className }}" t-on-keydown="followerListMenuView.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="followerListMenuView.isDisabled" t-on-click="followerListMenuView.onClickFollowersButton" title="Show Followers" t-att-class="{ 'o_ChatterTopbar_button': props.isChatterButton }">
<button class="o_FollowerListMenu_buttonFollowers o_ChatterTopbar_button btn btn-link" t-att-disabled="followerListMenuView.isDisabled" t-on-click="followerListMenuView.onClickFollowersButton" title="Show Followers">
<i class="fa fa-user"/>
<span class="o_FollowerListMenu_buttonFollowersCount pl-1" t-esc="thread.followers.length"/>
<span class="o_FollowerListMenu_buttonFollowersCount pl-1" t-esc="followerListMenuView.followerViews.length"/>
</button>
<t t-if="followerListMenuView.isDropdownOpen">
<div class="o_FollowerListMenu_dropdown dropdown-menu dropdown-menu-right flex-column d-flex" role="menu">
<t t-if="thread.model !== 'channel' and thread.hasWriteAccess">
<t t-if="followerListMenuView.chatterOwner.thread.model !== 'channel' and followerListMenuView.chatterOwner.thread.hasWriteAccess">
<a class="o_FollowerListMenu_addFollowersButton dropdown-item" href="#" role="menuitem" t-on-click="_onClickAddFollowers">
Add Followers
</a>
<t t-if="thread.followers.length > 0">
<t t-if="followerListMenuView.followerViews.length > 0">
<div role="separator" class="dropdown-divider"/>
</t>
</t>
<t t-if="thread.followers.length > 0">
<t t-if="followerListMenuView.followerViews.length > 0">
<Follower
t-foreach="thread.followers" t-as="follower" t-key="follower.localId"
t-foreach="followerListMenuView.followerViews" t-as="followerView" t-key="followerView.localId"
className="'o_FollowerMenu_follower dropdown-item'"
follower="follower"
onClick="followerListMenuView.onClickFollower"
record="followerView"
/>
</t>
<t t-elif="!thread.hasWriteAccess">
<t t-elif="!followerListMenuView.chatterOwner.thread.hasWriteAccess">
<div class="o_FollowerListMenu_noFollowers dropdown-item disabled">
No Followers
</div>
+4 -21
View File
@@ -46,27 +46,6 @@ 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();
},
/**
* @param {MouseEvent} ev
*/
onClickRemove(ev) {
this.remove();
},
/**
* Opens the most appropriate view that is a profile for this follower.
*/
@@ -200,6 +179,10 @@ registerModel({
inverse: 'followerOwnerAsSubtypeList',
isCausal: true,
}),
followerViews: many('FollowerView', {
inverse: 'follower',
isCausal: true,
}),
id: attr({
readonly: true,
required: true,
@@ -1,7 +1,8 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { attr, many, one } from '@mail/model/model_field';
import { insertAndReplace, replace } from '@mail/model/model_field_command';
registerModel({
name: 'FollowerListMenuView',
@@ -10,12 +11,6 @@ registerModel({
hide() {
this.update({ isDropdownOpen: false });
},
/**
* @param {MouseEvent} ev
*/
onClickFollower(ev) {
this.hide();
},
/**
* @param {MouseEvent} ev
*/
@@ -34,6 +29,15 @@ registerModel({
break;
}
},
/**
* @private
* @returns {FieldCommand}
*/
_computeFollowerViews() {
return insertAndReplace(this.chatterOwner.thread.followers.map(follower => {
return { follower: replace(follower) };
}));
},
/**
* @private
* @returns {Boolean}
@@ -47,6 +51,11 @@ registerModel({
inverse: 'followerListMenuView',
readonly: true,
}),
followerViews: many('FollowerView', {
compute: '_computeFollowerViews',
inverse: 'followerListMenuViewOwner',
isCausal: true,
}),
isDisabled: attr({
compute: '_computeIsDisabled',
}),
@@ -0,0 +1,50 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerModel({
name: 'FollowerView',
identifyingFields: ['followerListMenuViewOwner', 'follower'],
recordMethods: {
/**
* @param {MouseEvent} ev
*/
onClick(ev) {
this.followerListMenuViewOwner.hide();
},
/**
* @param {MouseEvent} ev
*/
onClickDetails(ev) {
ev.preventDefault();
ev.stopPropagation();
this.follower.openProfile();
},
/**
* @param {MouseEvent} ev
*/
onClickEdit(ev) {
ev.preventDefault();
this.follower.showSubtypes();
},
/**
* @param {MouseEvent} ev
*/
onClickRemove(ev) {
this.follower.remove();
},
},
fields: {
follower: one('Follower', {
inverse: 'followerViews',
readonly: true,
required: true,
}),
followerListMenuViewOwner: one('FollowerListMenuView', {
inverse: 'followerViews',
readonly: true,
required: true,
}),
},
});
@@ -533,7 +533,9 @@ MockServer.include({
* @returns {Object}
*/
async _mockRouteMailThreadData(thread_model, thread_id, request_list) {
const res = {};
const res = {
'hasWriteAccess': true, // mimic user with write access by default
};
const thread = this.pyEnv[thread_model].searchRead([['id', '=', thread_id]])[0];
if (!thread) {
console.warn(`mock server: reading data "${request_list}" from invalid thread "${thread_model}_${thread_id}"`);
@@ -1,11 +1,7 @@
/** @odoo-module **/
import { insert, replace } from '@mail/model/model_field_command';
import { makeDeferred } from '@mail/utils/deferred';
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import Bus from 'web.Bus';
@@ -16,23 +12,30 @@ QUnit.module('follower_tests.js');
QUnit.test('base rendering not editable', async function (assert) {
assert.expect(5);
const { createRootMessagingComponent, messaging } = await start();
const thread = messaging.models['Thread'].create({
hasWriteAccess: false,
id: 100,
model: 'res.partner',
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const follower = await messaging.models['Follower'].create({
partner: insert({
id: 1,
name: "François Perusse",
}),
followedThread: replace(thread),
id: 2,
isActive: true,
const { click, createChatterContainerComponent } = await start({
async mockRPC(route, args) {
if (route === '/mail/thread/data') {
// mimic user without write access
const res = await this._super(...arguments);
res['hasWriteAccess'] = false;
return res;
}
return this._super(...arguments);
},
});
await createRootMessagingComponent('Follower', { follower });
await createChatterContainerComponent({
threadId,
threadModel: 'res.partner',
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
@@ -63,22 +66,20 @@ QUnit.test('base rendering not editable', async function (assert) {
QUnit.test('base rendering editable', async function (assert) {
assert.expect(6);
const { createRootMessagingComponent, messaging } = await start();
const thread = messaging.models['Thread'].create({
hasWriteAccess: true,
id: 100,
model: 'res.partner',
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const follower = await messaging.models['Follower'].create({
partner: insert({
id: 1,
name: "François Perusse",
}),
followedThread: replace(thread),
id: 2,
isActive: true,
const { click, createChatterContainerComponent } = await start();
await createChatterContainerComponent({
threadId,
threadModel: 'res.partner',
});
await createRootMessagingComponent('Follower', { follower });
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
@@ -114,14 +115,22 @@ QUnit.test('base rendering editable', async function (assert) {
QUnit.test('click on partner follower details', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const openFormDef = makeDeferred();
const bus = new Bus();
bus.on('do-action', null, ({ action }) => {
assert.step('do_action');
assert.strictEqual(
action.res_id,
3,
"The redirect action should redirect to the right res id (3)"
partnerId,
"The redirect action should redirect to the right res id (partnerId)"
);
assert.strictEqual(
action.res_model,
@@ -135,24 +144,12 @@ QUnit.test('click on partner follower details', async function (assert) {
);
openFormDef.resolve();
});
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create();
const { createRootMessagingComponent, messaging } = await start({ env: { bus } });
const thread = messaging.models['Thread'].create({
id: resPartnerId1,
model: 'res.partner',
const { click, createChatterContainerComponent } = await start({ env: { bus } });
await createChatterContainerComponent({
threadId,
threadModel: 'res.partner',
});
const follower = await messaging.models['Follower'].create({
followedThread: replace(thread),
id: 2,
isActive: true,
partner: insert({
email: "bla@bla.bla",
id: messaging.currentPartner.id,
name: "François Perusse",
}),
});
await createRootMessagingComponent('Follower', { follower });
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
@@ -176,14 +173,14 @@ QUnit.test('click on edit follower', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create();
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: pyEnv.currentPartnerId,
res_id: resPartnerId1,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const { click, createRootMessagingComponent, messaging } = await start({
const { click, createChatterContainerComponent, messaging } = await start({
async mockRPC(route, args) {
if (route.includes('/mail/read_subscription_data')) {
assert.step('fetch_subtypes');
@@ -192,11 +189,15 @@ QUnit.test('click on edit follower', async function (assert) {
},
});
const thread = messaging.models['Thread'].create({
id: resPartnerId1,
id: threadId,
model: 'res.partner',
});
await thread.fetchData(['followers']);
await createRootMessagingComponent('Follower', { follower: thread.followers[0] });
await createChatterContainerComponent({
threadId,
threadModel: 'res.partner',
});
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',
@@ -224,8 +225,14 @@ QUnit.test('edit follower and close subtype dialog', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create();
const { click, createRootMessagingComponent, messaging } = await start({
const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]);
pyEnv['mail.followers'].create({
is_active: true,
partner_id: partnerId,
res_id: threadId,
res_model: 'res.partner',
});
const { click, createChatterContainerComponent } = await start({
async mockRPC(route, args) {
if (route.includes('/mail/read_subscription_data')) {
assert.step('fetch_subtypes');
@@ -241,21 +248,11 @@ QUnit.test('edit follower and close subtype dialog', async function (assert) {
return this._super(...arguments);
},
});
const thread = messaging.models['Thread'].create({
id: resPartnerId1,
model: 'res.partner',
await createChatterContainerComponent({
threadId,
threadModel: 'res.partner',
});
const follower = await messaging.models['Follower'].create({
followedThread: replace(thread),
id: 2,
isActive: true,
partner: insert({
email: "bla@bla.bla",
id: messaging.currentPartner.id,
name: "François Perusse",
}),
});
await createRootMessagingComponent('Follower', { follower });
await click('.o_FollowerListMenu_buttonFollowers');
assert.containsOnce(
document.body,
'.o_Follower',