[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:
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user