diff options
Diffstat (limited to 'addons/mail/static/src/components/follower')
4 files changed, 538 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/follower/follower.js b/addons/mail/static/src/components/follower/follower.js new file mode 100644 index 00000000..bafcd88a --- /dev/null +++ b/addons/mail/static/src/components/follower/follower.js @@ -0,0 +1,80 @@ +odoo.define('mail/static/src/components/follower/follower.js', function (require) { +'use strict'; + +const components = { + FollowerSubtypeList: require('mail/static/src/components/follower_subtype_list/follower_subtype_list.js'), +}; +const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { Component } = owl; + +class Follower extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useShouldUpdateBasedOnProps(); + useStore(props => { + const follower = this.env.models['mail.follower'].get(props.followerLocalId); + return [follower ? follower.__state : undefined]; + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {mail.follower} + */ + get follower() { + return this.env.models['mail.follower'].get(this.props.followerLocalId); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickDetails(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.follower.openProfile(); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickEdit(ev) { + ev.preventDefault(); + this.follower.showSubtypes(); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickRemove(ev) { + this.follower.remove(); + } + +} + +Object.assign(Follower, { + components, + props: { + followerLocalId: String, + }, + template: 'mail.Follower', +}); + +return Follower; + +}); diff --git a/addons/mail/static/src/components/follower/follower.scss b/addons/mail/static/src/components/follower/follower.scss new file mode 100644 index 00000000..509a119f --- /dev/null +++ b/addons/mail/static/src/components/follower/follower.scss @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ + +.o_Follower { + display: flex; + flex-flow: row; + justify-content: space-between; + padding: map-get($spacers, 0); +} + +.o_Follower_avatar { + width: 24px; + height: 24px; + margin-inline-end: map-get($spacers, 2); +} + +.o_Follower_details { + align-items: center; + display: flex; + flex: 1; + padding-left: map-get($spacers, 3); + padding-right: map-get($spacers, 3); +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + +.o_Follower_avatar { + border-radius: 50%; +} + +.o_Follower_button { + border-radius: 0; + + &:hover { + background: gray('400'); + color: $black; + } +} + +.o_Follower_details { + color: gray('700'); + + &:hover { + background: gray('400'); + color: $black; + } + + &.o-inactive { + opacity: 0.25; + font-style: italic; + } +} diff --git a/addons/mail/static/src/components/follower/follower.xml b/addons/mail/static/src/components/follower/follower.xml new file mode 100644 index 00000000..5cdc89d7 --- /dev/null +++ b/addons/mail/static/src/components/follower/follower.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="mail.Follower" owl="1"> + <div class="o_Follower"> + <t t-if="follower"> + <a class="o_Follower_details" t-att-class="{ 'o-inactive': !follower.isActive }" href="#" t-on-click="_onClickDetails"> + <img class="o_Follower_avatar" t-attf-src="/web/image/{{ follower.resModel }}/{{ follower.resId }}/image_128" alt="Avatar"/> + <span class="o_Follower_name" t-esc="follower.name or follower.displayName"/> + </a> + <t t-if="follower.isEditable"> + <button class="btn btn-icon o_Follower_button o_Follower_editButton" title="Edit subscription" t-on-click="_onClickEdit"> + <i class="fa fa-pencil"/> + </button> + <button class="btn btn-icon o_Follower_button o_Follower_removeButton" title="Remove this follower" t-on-click="_onClickRemove"> + <i class="fa fa-remove"/> + </button> + </t> + </t> + </div> + </t> + +</templates> diff --git a/addons/mail/static/src/components/follower/follower_tests.js b/addons/mail/static/src/components/follower/follower_tests.js new file mode 100644 index 00000000..28058fc9 --- /dev/null +++ b/addons/mail/static/src/components/follower/follower_tests.js @@ -0,0 +1,380 @@ +odoo.define('mail/static/src/components/follower/follower_tests.js', function (require) { +'use strict'; + +const components = { + Follower: require('mail/static/src/components/follower/follower.js'), +}; +const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js'); +const { + afterEach, + afterNextRender, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const Bus = require('web.Bus'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('follower', {}, function () { +QUnit.module('follower_tests.js', { + beforeEach() { + beforeEach(this); + + this.createFollowerComponent = async (follower) => { + await createRootComponent(this, components.Follower, { + props: { followerLocalId: follower.localId }, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('base rendering not editable', async function (assert) { + assert.expect(5); + + await this.start(); + + const thread = this.env.models['mail.thread'].create({ + id: 100, + model: 'res.partner', + }); + const follower = await this.env.models['mail.follower'].create({ + channel: [['insert', { id: 1, model: 'mail.channel', name: "François Perusse" }]], + followedThread: [['link', thread]], + id: 2, + isActive: true, + isEditable: false, + }); + await this.createFollowerComponent(follower); + assert.containsOnce( + document.body, + '.o_Follower', + "should have follower component" + ); + assert.containsOnce( + document.body, + '.o_Follower_details', + "should display a details part" + ); + assert.containsOnce( + document.body, + '.o_Follower_avatar', + "should display the avatar of the follower" + ); + assert.containsOnce( + document.body, + '.o_Follower_name', + "should display the name of the follower" + ); + assert.containsNone( + document.body, + '.o_Follower_button', + "should have no button as follower is not editable" + ); +}); + +QUnit.test('base rendering editable', async function (assert) { + assert.expect(6); + + await this.start(); + const thread = this.env.models['mail.thread'].create({ + id: 100, + model: 'res.partner', + }); + const follower = await this.env.models['mail.follower'].create({ + channel: [['insert', { id: 1, model: 'mail.channel', name: "François Perusse" }]], + followedThread: [['link', thread]], + id: 2, + isActive: true, + isEditable: true, + }); + await this.createFollowerComponent(follower); + assert.containsOnce( + document.body, + '.o_Follower', + "should have follower component" + ); + assert.containsOnce( + document.body, + '.o_Follower_details', + "should display a details part" + ); + assert.containsOnce( + document.body, + '.o_Follower_avatar', + "should display the avatar of the follower" + ); + assert.containsOnce( + document.body, + '.o_Follower_name', + "should display the name of the follower" + ); + assert.containsOnce( + document.body, + '.o_Follower_editButton', + "should have an edit button" + ); + assert.containsOnce( + document.body, + '.o_Follower_removeButton', + "should have a remove button" + ); +}); + +QUnit.test('click on channel follower details', async function (assert) { + assert.expect(7); + + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action.res_id, + 10, + "The redirect action should redirect to the right res id (10)" + ); + assert.strictEqual( + payload.action.res_model, + 'mail.channel', + "The redirect action should redirect to the right res model (mail.channel)" + ); + assert.strictEqual( + payload.action.type, + "ir.actions.act_window", + "The redirect action should be of type 'ir.actions.act_window'" + ); + }); + this.data['res.partner'].records.push({ id: 100 }); + this.data['mail.channel'].records.push({ id: 10 }); + await this.start({ + env: { bus }, + }); + const thread = this.env.models['mail.thread'].create({ + id: 100, + model: 'res.partner', + }); + const follower = await this.env.models['mail.follower'].create({ + channel: [['insert', { id: 10, model: 'mail.channel', name: "channel" }]], + followedThread: [['link', thread]], + id: 2, + isActive: true, + isEditable: true, + }); + await this.createFollowerComponent(follower); + assert.containsOnce( + document.body, + '.o_Follower', + "should have follower component" + ); + assert.containsOnce( + document.body, + '.o_Follower_details', + "should display a details part" + ); + + document.querySelector('.o_Follower_details').click(); + assert.verifySteps( + ['do_action'], + "clicking on channel should redirect to channel form view" + ); +}); + +QUnit.test('click on partner follower details', async function (assert) { + assert.expect(7); + + const openFormDef = makeDeferred(); + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action.res_id, + 3, + "The redirect action should redirect to the right res id (3)" + ); + assert.strictEqual( + payload.action.res_model, + 'res.partner', + "The redirect action should redirect to the right res model (res.partner)" + ); + assert.strictEqual( + payload.action.type, + "ir.actions.act_window", + "The redirect action should be of type 'ir.actions.act_window'" + ); + openFormDef.resolve(); + }); + this.data['res.partner'].records.push({ id: 100 }); + await this.start({ + env: { bus }, + }); + const thread = this.env.models['mail.thread'].create({ + id: 100, + model: 'res.partner', + }); + const follower = await this.env.models['mail.follower'].create({ + followedThread: [['link', thread]], + id: 2, + isActive: true, + isEditable: true, + partner: [['insert', { + email: "bla@bla.bla", + id: this.env.messaging.currentPartner.id, + name: "François Perusse", + }]], + }); + await this.createFollowerComponent(follower); + assert.containsOnce( + document.body, + '.o_Follower', + "should have follower component" + ); + assert.containsOnce( + document.body, + '.o_Follower_details', + "should display a details part" + ); + + document.querySelector('.o_Follower_details').click(); + await openFormDef; + assert.verifySteps( + ['do_action'], + "clicking on follower should redirect to partner form view" + ); +}); + +QUnit.test('click on edit follower', async function (assert) { + assert.expect(5); + + this.data['res.partner'].records.push({ id: 100, message_follower_ids: [2] }); + this.data['mail.followers'].records.push({ + id: 2, + is_active: true, + is_editable: true, + partner_id: this.data.currentPartnerId, + res_id: 100, + res_model: 'res.partner', + }); + await this.start({ + hasDialog: true, + async mockRPC(route, args) { + if (route.includes('/mail/read_subscription_data')) { + assert.step('fetch_subtypes'); + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].create({ + id: 100, + model: 'res.partner', + }); + await thread.refreshFollowers(); + await this.createFollowerComponent(thread.followers[0]); + assert.containsOnce( + document.body, + '.o_Follower', + "should have follower component" + ); + assert.containsOnce( + document.body, + '.o_Follower_editButton', + "should display an edit button" + ); + + await afterNextRender(() => document.querySelector('.o_Follower_editButton').click()); + assert.verifySteps( + ['fetch_subtypes'], + "clicking on edit follower should fetch subtypes" + ); + assert.containsOnce( + document.body, + '.o_FollowerSubtypeList', + "A dialog allowing to edit follower subtypes should have been created" + ); +}); + +QUnit.test('edit follower and close subtype dialog', async function (assert) { + assert.expect(6); + + this.data['res.partner'].records.push({ id: 100 }); + await this.start({ + hasDialog: true, + async mockRPC(route, args) { + if (route.includes('/mail/read_subscription_data')) { + assert.step('fetch_subtypes'); + return [{ + default: true, + followed: true, + internal: false, + id: 1, + name: "Dummy test", + res_model: 'res.partner' + }]; + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].create({ + id: 100, + model: 'res.partner', + }); + const follower = await this.env.models['mail.follower'].create({ + followedThread: [['link', thread]], + id: 2, + isActive: true, + isEditable: true, + partner: [['insert', { + email: "bla@bla.bla", + id: this.env.messaging.currentPartner.id, + name: "François Perusse", + }]], + }); + await this.createFollowerComponent(follower); + assert.containsOnce( + document.body, + '.o_Follower', + "should have follower component" + ); + assert.containsOnce( + document.body, + '.o_Follower_editButton', + "should display an edit button" + ); + + await afterNextRender(() => document.querySelector('.o_Follower_editButton').click()); + assert.verifySteps( + ['fetch_subtypes'], + "clicking on edit follower should fetch subtypes" + ); + assert.containsOnce( + document.body, + '.o_FollowerSubtypeList', + "dialog allowing to edit follower subtypes should have been created" + ); + + await afterNextRender( + () => document.querySelector('.o_FollowerSubtypeList_closeButton').click() + ); + assert.containsNone( + document.body, + '.o_DialogManager_dialog', + "follower subtype dialog should be closed after clicking on close button" + ); +}); + +}); +}); +}); + +}); |
