[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:
committed by
Aaron Bohy
parent
f46392707d
commit
d25459e6cb
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
-3
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user