[IMP] mail,web: add many2many_avatar_user widget to open chat

with this commit, we adds many2many_avatar_user widget to have same behaviour
as many2one_avatar_user where clicking on avatar image opens chat box, so to
have same feature on many2many_tags which is related to res.users we adds
many2many_avatar_user widget.

task-2451204
This commit is contained in:
Mohammed Shekha
2021-05-26 14:04:15 +00:00
committed by Aaron Bohy
parent f46392707d
commit d25459e6cb
7 changed files with 217 additions and 80 deletions
+3 -3
View File
@@ -58,7 +58,7 @@
'web.assets_backend': [
'mail/static/src/js/core/translation.js',
'mail/static/src/js/many2many_tags_email.js',
'mail/static/src/js/many2one_avatar_user.js',
'mail/static/src/js/m2x_avatar_user.js',
'mail/static/src/js/field_char.js',
'mail/static/src/js/document_viewer.js',
'mail/static/src/js/basic_view.js',
@@ -86,7 +86,7 @@
'mail/static/src/scss/thread.scss',
'mail/static/src/scss/systray.scss',
'mail/static/src/scss/mail_activity.scss',
'mail/static/src/scss/many2one_avatar_user.scss',
'mail/static/src/scss/m2x_avatar_user.scss',
'mail/static/src/scss/activity_view.scss',
'mail/static/src/scss/kanban_view.scss',
'mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js',
@@ -280,7 +280,7 @@
'mail/static/tests/chatter_tests.js',
'mail/static/tests/mail_utils_tests.js',
'mail/static/tests/document_viewer_tests.js',
'mail/static/tests/many2one_avatar_user_tests.js',
'mail/static/tests/m2x_avatar_user_tests.js',
'mail/static/tests/systray/systray_activity_menu_tests.js',
'mail/static/tests/tools/debug_manager_tests.js',
'mail/static/tests/activity_tests.js',
@@ -0,0 +1,100 @@
/** @odoo-module **/
import fieldRegistry from 'web.field_registry';
import { Many2OneAvatar } from 'web.relational_fields';
import { FieldMany2ManyTagsAvatar } from 'web.relational_fields';
const { Component } = owl;
// This module defines extensions of the Many2OneAvatar and Many2ManyAvatar
// widgets, which are integrated with the messaging system. They are designed
// to display people, and when the avatar of those people is clicked, it
// opens a DM chat window with the corresponding user.
//
// These widgets are supported on many2one and many2many fields pointing to
// 'res.users'.
//
// Usage:
// <field name="user_id" widget="many2one_avatar_user"/>
//
// The widgets are designed to be extended, to support fields pointing to other
// models than 'res.users'.
const M2XAvatarMixin = {
supportedModels: ['res.users'],
init() {
this._super(...arguments);
if (!this.supportedModels.includes(this.field.relation)) {
throw new Error(`This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(this.supportedModels)}`);
}
this.className += ' o_clickable_m2x_avatar';
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Opens a chat window with the given user id.
*
* @private
* @param {Object} params
* @returns {Promise}
*/
_openChat(params) {
const env = Component.env;
return env.messaging.openChat(params);
},
};
export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
events: Object.assign({}, Many2OneAvatar.prototype.events, {
'click .o_m2o_avatar > img': '_onAvatarClicked',
}),
//----------------------------------------------------------------------
// Handlers
//----------------------------------------------------------------------
/**
* When the avatar is clicked, open a DM chat window with the
* corresponding user.
*
* @private
* @param {MouseEvent} ev
*/
_onAvatarClicked(ev) {
ev.stopPropagation(); // in list view, prevent from opening the record
this._openChat({ userId: this.value.res_id });
}
});
export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({
_template: 'mail.KanbanMany2OneAvatarUser',
});
export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2XAvatarMixin, {
events: Object.assign({}, FieldMany2ManyTagsAvatar.prototype.events, {
'click .o_m2m_avatar': '_onAvatarClicked',
}),
//----------------------------------------------------------------------
// Handlers
//----------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
*/
_onAvatarClicked(ev) {
ev.stopPropagation(); // in list view, prevent from opening the record
const userId = parseInt(ev.target.getAttribute('data-id'), 10);
this._openChat({ userId: userId });
},
});
fieldRegistry.add('many2one_avatar_user', Many2OneAvatarUser);
fieldRegistry.add('kanban.many2one_avatar_user', KanbanMany2OneAvatarUser);
fieldRegistry.add('many2many_avatar_user', Many2ManyAvatarUser);
@@ -1,63 +0,0 @@
/** @odoo-module **/
// This module defines an extension of the Many2OneAvatar widget, which is
// integrated with the messaging system. The Many2OneAvatarUser is designed
// to display people, and when the avatar of those people is clicked, it
// opens a DM chat window with the corresponding user.
//
// This widget is supported on many2one fields pointing to 'res.users'.
//
// Usage:
// <field name="user_id" widget="many2one_avatar_user"/>
//
// The widget is designed to be extended, to support many2one fields pointing
// to other models than 'res.users'.
import fieldRegistry from 'web.field_registry';
import { Many2OneAvatar } from 'web.relational_fields';
const { Component } = owl;
const Many2OneAvatarUser = Many2OneAvatar.extend({
events: Object.assign({}, Many2OneAvatar.prototype.events, {
'click .o_m2o_avatar > img': '_onAvatarClicked',
}),
// This widget is only supported on many2ones pointing to 'res.users'
supportedModels: ['res.users'],
init() {
this._super(...arguments);
if (!this.supportedModels.includes(this.field.relation)) {
throw new Error(`This widget is only supported on many2one fields pointing to ${JSON.stringify(this.supportedModels)}`);
}
},
//----------------------------------------------------------------------
// Handlers
//----------------------------------------------------------------------
/**
* When the avatar is clicked, open a DM chat window with the
* corresponding user.
*
* @private
* @param {MouseEvent} ev
*/
async _onAvatarClicked(ev) {
ev.stopPropagation(); // in list view, prevent from opening the record
const env = Component.env;
await env.messaging.openChat({ userId: this.value.res_id });
}
});
const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({
_template: 'mail.KanbanMany2OneAvatarUser',
});
fieldRegistry.add('many2one_avatar_user', Many2OneAvatarUser);
fieldRegistry.add('kanban.many2one_avatar_user', KanbanMany2OneAvatarUser);
export {
Many2OneAvatarUser,
KanbanMany2OneAvatarUser,
};
@@ -0,0 +1,19 @@
@mixin o-avatar-image-hover {
cursor: pointer;
filter: brightness(0.8);
}
.o_field_many2one_avatar.o_clickable_m2x_avatar {
.o_m2o_avatar:hover {
@include o-avatar-image-hover;
}
}
.o_field_many2manytags.avatar.o_clickable_m2x_avatar {
img.o_m2m_avatar {
margin-right: 2px;
&:hover {
@include o-avatar-image-hover;
}
}
}
@@ -2,14 +2,15 @@
import { afterEach, beforeEach, start } from '@mail/utils/test_utils';
import FormView from 'web.FormView';
import KanbanView from 'web.KanbanView';
import ListView from 'web.ListView';
import { Many2OneAvatarUser } from '@mail/js/many2one_avatar_user';
import { Many2OneAvatarUser } from '@mail/js/m2x_avatar_user';
import { dom, mock } from 'web.test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('Many2OneAvatarUser', {
QUnit.module('M2XAvatarUser', {
beforeEach() {
beforeEach(this);
@@ -20,9 +21,10 @@ QUnit.module('mail', {}, function () {
'foo': {
fields: {
user_id: { string: "User", type: 'many2one', relation: 'res.users' },
user_ids: { string: "Users", type: "many2many", relation: 'res.users' },
},
records: [
{ id: 1, user_id: 11 },
{ id: 1, user_id: 11, user_ids: [11, 23], },
{ id: 2, user_id: 7 },
{ id: 3, user_id: 11 },
{ id: 4, user_id: 23 },
@@ -118,4 +120,82 @@ QUnit.module('mail', {}, function () {
kanban.destroy();
});
QUnit.test('many2many_avatar_user widget in form view', async function (assert) {
assert.expect(7);
const { widget: form } = await start({
hasView: true,
View: FormView,
model: 'foo',
data: this.data,
arch: '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
mockRPC(route, args) {
if (args.method === 'read') {
assert.step(`read ${args.model} ${args.args[0]}`);
}
return this._super(...arguments);
},
res_id: 1,
});
assert.containsN(form, '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
"should have 2 records");
assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'), '/web/image/res.users/11/image_128',
"should have correct avatar image");
await dom.click(form.$('.o_field_many2manytags.avatar .badge:first .o_m2m_avatar'));
await dom.click(form.$('.o_field_many2manytags.avatar .badge:nth(1) .o_m2m_avatar'));
assert.verifySteps([
"read foo 1",
'read res.users 11,23',
"read res.users 11",
"read res.users 23",
]);
form.destroy();
});
QUnit.test('many2many_avatar_user widget in list view', async function (assert) {
assert.expect(7);
const { widget: list } = await start({
hasView: true,
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="user_ids" widget="many2many_avatar_user"/></tree>',
mockRPC(route, args) {
if (args.method === 'read') {
assert.step(`read ${args.model} ${args.args[0]}`);
}
return this._super(...arguments);
},
});
mock.intercept(list, 'open_record', () => {
assert.step('open record');
});
assert.containsN(list.$(".o_data_cell:first"), '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
"should have 2 records");
assert.strictEqual(list.$('.o_data_cell:first .o_tag_badge_text').text().trim(), 'MarioYoshi');
// sanity check: later on, we'll check that clicking on the avatar doesn't open the record
await dom.click(list.$('.o_data_row:first .o_field_many2manytags'));
await dom.click(list.$('.o_data_cell:nth(0) .o_m2m_avatar:nth(0)'));
await dom.click(list.$('.o_data_cell:nth(0) .o_m2m_avatar:nth(1)'));
assert.verifySteps([
'read res.users 11,23',
"open record",
"read res.users 11",
"read res.users 23",
]);
list.destroy();
});
});
+10 -9
View File
@@ -229,27 +229,28 @@
}
// Many2many tags avatar
&.avatar {
align-items: center;
.badge {
padding-left: 4px;
padding: 0 0.6em 0 0;
img {
height: 18px;
width: 18px;
height: 17px;
width: 17px;
object-fit: cover;
}
&.o_tag_color_0 {
background-color: #F1F1F1;
box-shadow: none;
border: 1px solid gray('300');
margin-top: 1px;
margin-left: 1px;
}
.o_badge_text {
display: flex;
align-items:center;
padding-top: 0.25em;
padding-bottom: 0.25em;
}
.o_tag_badge_text {
padding-left:2px;
}
.o_delete {
margin-top: 3px;
padding-top: 0.25em;
padding-bottom: 0.25em;
}
}
}
+2 -2
View File
@@ -1311,8 +1311,8 @@
</t>
</t>
<t t-name="FieldMany2ManyTagAvatar" t-extend="FieldMany2ManyTag">
<t t-jquery="span:first" t-operation="prepend">
<img t-attf-src="/web/image/#{avatarModel}/#{el.id}/#{avatarField}" class="rounded-circle"/>
<t t-jquery="t[t-set='_badge_text']" t-operation="before">
<span><img t-attf-src="/web/image/#{avatarModel}/#{el.id}/#{avatarField}" t-att-data-id="el.id" class="rounded-circle o_m2m_avatar"/></span>
</t>
</t>
<t t-name="FieldMany2ManyTag.colorpicker">