[IMP] mail: introduce 'FollowerSubtypeView' model

Task-2793280

closes odoo/odoo#87011

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-03-29 20:21:01 +02:00
parent 9207b7aeea
commit 3f75153ad7
7 changed files with 240 additions and 137 deletions
@@ -24,6 +24,13 @@ export class FollowerSubtype extends Component {
return this.messaging && this.messaging.models['FollowerSubtype'].get(this.props.followerSubtypeLocalId);
}
/**
* @returns {FollowerSubtypeView}
*/
get followerSubtypeView() {
return this.messaging && this.messaging.models['FollowerSubtypeView'].get(this.props.localId);
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -48,6 +55,7 @@ Object.assign(FollowerSubtype, {
props: {
followerLocalId: String,
followerSubtypeLocalId: String,
localId: String,
},
template: 'mail.FollowerSubtype',
});
@@ -2,8 +2,8 @@
<templates xml:space="preserve">
<t t-name="mail.FollowerSubtype" owl="1">
<t t-if="followerSubtype">
<div class="o_FollowerSubtype" t-attf-class="{{ className }}" t-ref="root">
<t t-if="followerSubtypeView">
<div class="o_FollowerSubtype" t-attf-class="{{ className }}" t-att-data-local-id="followerSubtypeView.localId" t-ref="root">
<label class="o_FollowerSubtype_label">
<input class="o_FollowerSubtype_checkbox" type="checkbox" t-att-checked="follower.selectedSubtypes.includes(followerSubtype) ? 'checked': ''" t-on-change="_onChangeCheckbox"/>
<t t-esc="followerSubtype.name"/>
@@ -13,11 +13,12 @@
</header>
<main class="modal-body">
<div class="o_FollowerSubtypeList_subtypes">
<t t-foreach="followerSubtypeList.follower.subtypes" t-as="subtype" t-key="subtype.id">
<t t-foreach="followerSubtypeList.followerSubtypeViews" t-as="followerSubtypeView" t-key="followerSubtypeView.localId">
<FollowerSubtype
className="'o_FollowerSubtypeList_subtype'"
localId="followerSubtypeView.localId"
followerLocalId="followerSubtypeList.follower.localId"
followerSubtypeLocalId="subtype.localId"
followerSubtypeLocalId="followerSubtypeView.subtype.localId"
/>
</t>
</div>
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr } from '@mail/model/model_field';
import { attr, many } from '@mail/model/model_field';
registerModel({
name: 'FollowerSubtype',
@@ -38,6 +38,10 @@ registerModel({
},
},
fields: {
followerSubtypeViews: many('FollowerSubtypeView', {
inverse: 'subtype',
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 { clear, insertAndReplace, replace } from '@mail/model/model_field_command';
registerModel({
name: 'FollowerSubtypeList',
@@ -16,6 +17,25 @@ registerModel({
containsElement(element) {
return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element));
},
/**
* @private
* @returns {FieldCommand}
*/
_computeFollowerSubtypeViews() {
if (this.follower.subtypes.length === 0) {
return clear();
}
return insertAndReplace(this.follower.subtypes.map(subtype => ({ subtype: replace(subtype) })));
},
/**
* @private
* @returns {Array}
*/
_sortFollowerSubtypeViews() {
return [
['smaller-first', 'subtype.id'],
];
},
},
fields: {
/**
@@ -34,5 +54,11 @@ registerModel({
related: 'dialogOwner.followerOwnerAsSubtypeList',
required: true,
}),
followerSubtypeViews: many('FollowerSubtypeView', {
compute: '_computeFollowerSubtypeViews',
inverse: 'followerSubtypeListOwner',
isCausal: true,
sort: '_sortFollowerSubtypeViews',
}),
},
});
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerModel({
name: 'FollowerSubtypeView',
identifyingFields: ['followerSubtypeListOwner', 'subtype'],
fields: {
followerSubtypeListOwner: one('FollowerSubtypeList', {
inverse: 'followerSubtypeViews',
readonly: true,
required: true,
}),
subtype: one('FollowerSubtype', {
inverse: 'followerSubtypeViews',
readonly: true,
required: true,
}),
},
});
@@ -1,12 +1,15 @@
/** @odoo-module **/
import { insert, link } from '@mail/model/model_field_command';
import { makeDeferred } from '@mail/utils/deferred';
import {
afterNextRender,
createRootMessagingComponent,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import FormView from 'web.FormView';
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('follower_subtype_tests.js', {
@@ -21,185 +24,225 @@ QUnit.module('follower_subtype_tests.js', {
target,
});
};
// FIXME archs could be removed once task-2248306 is done
// The mockServer will try to get the list view
// of every relational fields present in the main view.
// In the case of mail fields, we don't really need them,
// but they still need to be defined.
this.createView = async (viewParams, ...args) => {
const startResult = makeDeferred();
await afterNextRender(async () => {
const viewArgs = Object.assign(
{
archs: {
'mail.activity,false,list': '<tree/>',
'mail.followers,false,list': '<tree/>',
'mail.message,false,list': '<tree/>',
},
},
viewParams,
);
startResult.resolve(await start(viewArgs, ...args));
});
return startResult;
};
},
});
QUnit.test('simplest layout of a followed subtype', async function (assert) {
assert.expect(5);
const { messaging, widget } = await start();
const thread = messaging.models['Thread'].create({
id: 100,
const pyEnv = await startServer();
const subtypeId = pyEnv['mail.message.subtype'].create({
default: true,
name: 'TestSubtype',
});
const followerId = pyEnv['mail.followers'].create({
display_name: "François Perusse",
partner_id: pyEnv.currentPartnerId,
res_model: 'res.partner',
res_id: pyEnv.currentPartnerId,
subtype_ids: [subtypeId],
});
pyEnv['res.partner'].write([pyEnv.currentPartnerId], {
message_follower_ids: [followerId],
});
const { click } = await this.createView({
hasDialog: true,
hasView: true,
// View params
View: FormView,
model: 'res.partner',
arch: `
<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
res_id: pyEnv.currentPartnerId,
// FIXME: should adapt mock server code to provide `hasWriteAccess`
async mockRPC(route, args) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await this._super(...arguments);
res['hasWriteAccess'] = true;
return res;
}
return this._super(...arguments);
},
});
const follower = messaging.models['Follower'].create({
partner: insert({
id: 1,
name: "François Perusse",
}),
followedThread: link(thread),
id: 2,
isActive: true,
});
const followerSubtype = messaging.models['FollowerSubtype'].create({
id: 1,
isDefault: true,
isInternal: false,
name: "Dummy test",
resModel: 'res.partner'
});
follower.update({
selectedSubtypes: link(followerSubtype),
subtypes: link(followerSubtype),
});
await this.createFollowerSubtypeComponent({
follower,
followerSubtype,
target: widget.el,
});
await click('.o_FollowerListMenu_buttonFollowers');
await click('.o_Follower_editButton');
assert.containsOnce(
document.body,
'.o_FollowerSubtype',
"should have follower subtype component"
'.o_FollowerSubtype:contains(TestSubtype)',
"should have a follower subtype for 'TestSubtype'"
);
assert.containsOnce(
document.body,
document.querySelector('.o_FollowerSubtype'),
'.o_FollowerSubtype_label',
"should have a label"
);
assert.containsOnce(
document.body,
$('.o_FollowerSubtype:contains(TestSubtype)'),
'.o_FollowerSubtype_checkbox',
"should have a checkbox"
);
assert.strictEqual(
document.querySelector('.o_FollowerSubtype_label').textContent,
"Dummy test",
$('.o_FollowerSubtype:contains(TestSubtype) .o_FollowerSubtype_label')[0].textContent,
"TestSubtype",
"should have the name of the subtype as label"
);
assert.ok(
document.querySelector('.o_FollowerSubtype_checkbox').checked,
$('.o_FollowerSubtype:contains(TestSubtype) .o_FollowerSubtype_checkbox')[0].checked,
"checkbox should be checked as follower subtype is followed"
);
});
QUnit.test('simplest layout of a not followed subtype', async function (assert) {
assert.expect(5);
assert.expect(1);
const { messaging, widget } = await start();
const thread = messaging.models['Thread'].create({
id: 100,
const pyEnv = await startServer();
pyEnv['mail.message.subtype'].create({
default: true,
name: 'TestSubtype',
});
const followerId = pyEnv['mail.followers'].create({
display_name: "François Perusse",
partner_id: pyEnv.currentPartnerId,
res_model: 'res.partner',
res_id: pyEnv.currentPartnerId,
});
pyEnv['res.partner'].write([pyEnv.currentPartnerId], {
message_follower_ids: [followerId],
});
const { click } = await this.createView({
hasDialog: true,
hasView: true,
// View params
View: FormView,
model: 'res.partner',
arch: `
<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
res_id: pyEnv.currentPartnerId,
// FIXME: should adapt mock server code to provide `hasWriteAccess`
async mockRPC(route, args) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await this._super(...arguments);
res['hasWriteAccess'] = true;
return res;
}
return this._super(...arguments);
},
});
const follower = messaging.models['Follower'].create({
partner: insert({
id: 1,
name: "François Perusse",
}),
followedThread: link(thread),
id: 2,
isActive: true,
});
const followerSubtype = messaging.models['FollowerSubtype'].create({
id: 1,
isDefault: true,
isInternal: false,
name: "Dummy test",
resModel: 'res.partner'
});
follower.update({ subtypes: link(followerSubtype) });
await this.createFollowerSubtypeComponent({
follower,
followerSubtype,
target: widget.el,
});
assert.containsOnce(
document.body,
'.o_FollowerSubtype',
"should have follower subtype component"
);
assert.containsOnce(
document.body,
'.o_FollowerSubtype_label',
"should have a label"
);
assert.containsOnce(
document.body,
'.o_FollowerSubtype_checkbox',
"should have a checkbox"
);
assert.strictEqual(
document.querySelector('.o_FollowerSubtype_label').textContent,
"Dummy test",
"should have the name of the subtype as label"
);
await click('.o_FollowerListMenu_buttonFollowers');
await click('.o_Follower_editButton');
assert.notOk(
document.querySelector('.o_FollowerSubtype_checkbox').checked,
$('.o_FollowerSubtype:contains(TestSubtype) .o_FollowerSubtype_checkbox')[0].checked,
"checkbox should not be checked as follower subtype is not followed"
);
});
QUnit.test('toggle follower subtype checkbox', async function (assert) {
assert.expect(5);
assert.expect(3);
const { messaging, widget } = await start();
const thread = messaging.models['Thread'].create({
id: 100,
const pyEnv = await startServer();
pyEnv['mail.message.subtype'].create({
default: true,
name: 'TestSubtype',
});
const followerId = pyEnv['mail.followers'].create({
display_name: "François Perusse",
partner_id: pyEnv.currentPartnerId,
res_model: 'res.partner',
res_id: pyEnv.currentPartnerId,
});
pyEnv['res.partner'].write([pyEnv.currentPartnerId], {
message_follower_ids: [followerId],
});
const { click, messaging } = await this.createView({
hasDialog: true,
hasView: true,
// View params
View: FormView,
model: 'res.partner',
arch: `
<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
res_id: pyEnv.currentPartnerId,
// FIXME: should adapt mock server code to provide `hasWriteAccess`
async mockRPC(route, args) {
if (route === '/mail/thread/data') {
// mimic user with write access
const res = await this._super(...arguments);
res['hasWriteAccess'] = true;
return res;
}
return this._super(...arguments);
},
});
const follower = messaging.models['Follower'].create({
partner: insert({
id: 1,
name: "François Perusse",
}),
followedThread: link(thread),
id: 2,
isActive: true,
});
const followerSubtype = messaging.models['FollowerSubtype'].create({
id: 1,
isDefault: true,
isInternal: false,
name: "Dummy test",
resModel: 'res.partner'
});
follower.update({ subtypes: link(followerSubtype) });
await this.createFollowerSubtypeComponent({
follower,
followerSubtype,
target: widget.el,
});
assert.containsOnce(
document.body,
'.o_FollowerSubtype',
"should have follower subtype component"
);
assert.containsOnce(
document.body,
'.o_FollowerSubtype_checkbox',
"should have a checkbox"
);
await click('.o_FollowerListMenu_buttonFollowers');
await click('.o_Follower_editButton');
const followerSubtype = messaging.models['FollowerSubtype'].find(subtype => subtype.name === 'TestSubtype');
const followerSubtypeView = followerSubtype.followerSubtypeViews[0];
assert.notOk(
document.querySelector('.o_FollowerSubtype_checkbox').checked,
document.querySelector(`.o_FollowerSubtype[data-local-id="${followerSubtypeView.localId}"] .o_FollowerSubtype_checkbox`).checked,
"checkbox should not be checked as follower subtype is not followed"
);
await afterNextRender(() =>
document.querySelector('.o_FollowerSubtype_checkbox').click()
);
await click(`.o_FollowerSubtype[data-local-id="${followerSubtypeView.localId}"] .o_FollowerSubtype_checkbox`);
assert.ok(
document.querySelector('.o_FollowerSubtype_checkbox').checked,
document.querySelector(`.o_FollowerSubtype[data-local-id="${followerSubtypeView.localId}"] .o_FollowerSubtype_checkbox`).checked,
"checkbox should now be checked"
);
await afterNextRender(() =>
document.querySelector('.o_FollowerSubtype_checkbox').click()
);
await click(`.o_FollowerSubtype[data-local-id="${followerSubtypeView.localId}"] .o_FollowerSubtype_checkbox`);
assert.notOk(
document.querySelector('.o_FollowerSubtype_checkbox').checked,
document.querySelector(`.o_FollowerSubtype[data-local-id="${followerSubtypeView.localId}"] .o_FollowerSubtype_checkbox`).checked,
"checkbox should be no more checked"
);
});