[IMP] web, mail: open the chat window when clicking on an avatar property

Purpose
=======
Like standard avatar widget, we want to open the chat window of a user
when clicking on a many2many / many2one avatar property.

Task-3208449

Part-of: odoo/odoo#114200
This commit is contained in:
std-odoo
2023-05-17 13:37:37 +02:00
parent 04192be40a
commit 09095db293
14 changed files with 216 additions and 38 deletions
+4 -1
View File
@@ -14,7 +14,10 @@ class IrModel(models.Model):
fields_data_by_fname = {
fname: field_data
for fname, field_data in model.fields_get(
attributes=['name', 'type', 'relation', 'required', 'readonly', 'selection', 'string']
attributes={
'name', 'type', 'relation', 'required', 'readonly', 'selection',
'string', 'definition_record', 'definition_record_field',
},
).items()
if not field_data.get('relation') or field_data['relation'] in model_names_to_fetch
}
@@ -0,0 +1,54 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { PropertyValue } from "@web/views/fields/properties/property_value";
import { TagsList } from "@web/core/tags_list/tags_list";
import { useOpenChat } from "@mail/web/open_chat_hook";
/**
* Allow to open the chatter of the user when we click on the avatar of a Many2one
* property (like we do for many2one_avatar_user widget).
*/
patch(PropertyValue.prototype, "mail.PropertyValue", {
setup() {
this._super();
if (this.env.services["mail.thread"]) {
// work only for the res.users model
this.openChat = useOpenChat("res.users");
}
},
_onAvatarClicked() {
if (this.openChat && this.showAvatar && this.props.comodel === "res.users") {
this.openChat(this.props.value[0]);
}
},
});
/**
* Allow to open the chatter of the user when we click on the avatar of a Many2many
* property (like we do for many2many_avatar_user widget).
*/
export class Many2manyPropertiesTagsList extends TagsList {
static template = "mail.Many2manyPropertiesTagsList";
setup() {
super.setup();
if (this.env.services["mail.thread"]) {
this.openChat = useOpenChat("res.users");
}
}
_onAvatarClicked(tagIndex) {
const tag = this.props.tags[tagIndex];
if (this.openChat && tag.comodel === "res.users") {
this.openChat(tag.id);
}
}
}
PropertyValue.components = {
...PropertyValue.components,
TagsList: Many2manyPropertiesTagsList,
};
@@ -0,0 +1,13 @@
.o_property_field_value {
.o_field_property_many2one_value img[comodel="res.users"],
.o_m2m_avatar[comodel="res.users"] {
height: var(--Avatar-size, 1.7145em);
width: auto;
aspect-ratio: 1;
&:hover {
cursor: context-menu;
outline: $border-width solid $o-action;
}
}
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<templates xml:space="preserve">
<!-- Many2one -->
<t t-inherit="web.PropertyValue" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_field_property_many2one_value')]//img" position="attributes">
<attribute name="t-on-click.prevent.stop">_onAvatarClicked</attribute>
<attribute name="t-att-comodel">props.comodel</attribute>
</xpath>
</t>
<!-- Many2many -->
<t t-name="mail.Many2manyPropertiesTagsList" t-inherit="web.TagsList"
t-inherit-mode="primary" owl="1">
<img position="attributes">
<attribute name="t-on-click.prevent.stop">() => this._onAvatarClicked(tag_index)</attribute>
<attribute name="t-att-comodel">tag.comodel</attribute>
</img>
</t>
</templates>
@@ -0,0 +1,13 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { Record } from "@web/views/relational_model";
patch(Record.prototype, "mail.Record", {
_getWidgetFromDefinition(definition) {
if (definition.comodel !== "res.users") {
return this._super(...arguments);
}
return definition.type === "many2one" ? "many2one_avatar_user" : "many2many_avatar_user";
},
});
+1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_discuss_models
from . import test_mail_corner_case_models
from . import test_mail_models
from . import test_mail_thread_models
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class MailTestProperties(models.Model):
_description = 'Mail Test Properties'
_name = 'mail.test.properties'
_inherit = ['mail.thread']
name = fields.Char('Name')
parent_id = fields.Many2one('mail.test.properties', string='Parent')
properties = fields.Properties('Properties', definition='parent_id.definition_properties')
definition_properties = fields.PropertiesDefinition('Definitions')
@@ -46,3 +46,4 @@ access_mail_test_track_compute,mail.test.track.compute,model_mail_test_track_com
access_mail_test_track_monetary,mail.test.track.monetary,model_mail_test_track_monetary,base.group_user,1,1,1,1
access_mail_test_track_selection_portal,mail.test.track.selection.portal,model_mail_test_track_selection,base.group_portal,0,0,0,0
access_mail_test_track_selection_user,mail.test.track.selection.user,model_mail_test_track_selection,base.group_user,1,1,1,1
access_mail_test_properties_user,mail.test.properties.user,model_mail_test_properties,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
46 access_mail_test_track_monetary mail.test.track.monetary model_mail_test_track_monetary base.group_user 1 1 1 1
47 access_mail_test_track_selection_portal mail.test.track.selection.portal model_mail_test_track_selection base.group_portal 0 0 0 0
48 access_mail_test_track_selection_user mail.test.track.selection.user model_mail_test_track_selection base.group_user 1 1 1 1
49 access_mail_test_properties_user mail.test.properties.user model_mail_test_properties base.group_user 1 1 1 1
@@ -7,4 +7,5 @@ addModelNamesToFetch([
"mail.test.activity",
"mail.test.multi.company",
"mail.test.multi.company.read",
"mail.test.properties",
]);
@@ -0,0 +1,77 @@
/** @odoo-module **/
import { start, startServer, click } from "@mail/../tests/helpers/test_utils";
QUnit.module("properties field");
/**
* Open a chat window when clicking on an avatar many2one / many2many properties.
*/
async function testPropertyFieldAvatarOpenChat(assert, propertyType) {
const pyEnv = await startServer();
const view = `
<form string="Form With Avatar Users">
<sheet>
<field name="name"/>
<field name="parent_id"/>
<field name="properties"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>`;
const { openView } = await start({
serverData: {
views: {
"mail.test.properties,false,form": view,
},
},
async mockRPC(route) {
if (route.includes("/mail.test.properties/check_access_rights")) {
return true;
} else if (route === "/web/dataset/call_kw/res.users/read") {
assert.step("read res.users");
return [{ id: userId, partner_id: [partnerId, "Partner Test"] }];
} else if (route === "/web/dataset/call_kw/res.users/search_read") {
return [{ id: userId, name: "User Test" }];
}
},
});
const partnerId = pyEnv["res.partner"].create({ name: "Partner Test" });
const userId = pyEnv["res.users"].create({ name: "User Test" });
const parentId = pyEnv["mail.test.properties"].create({ name: "Parent" });
const value = propertyType === "many2one" ? [userId, "User Test"] : [[userId, "User Test"]];
const childId = pyEnv["mail.test.properties"].create({
name: "Test",
parent_id: parentId,
properties: [
{ type: propertyType, comodel: "res.users", name: "user", string: "user", value },
],
});
await openView({
res_id: childId,
res_model: "mail.test.properties",
views: [[false, "form"]],
});
assert.verifySteps([]);
await click(
propertyType === "many2one" ? ".o_field_property_many2one_value img" : ".o_m2m_avatar"
);
assert.verifySteps(["read res.users"]);
const chatter = document.querySelectorAll(".o-mail-ChatWindow-name");
assert.strictEqual(chatter.length, 1);
assert.strictEqual(chatter[0].innerText, "Partner Test");
}
QUnit.test("Properties fields: many2one avatar open chat on click", async function (assert) {
await testPropertyFieldAvatarOpenChat(assert, "many2one");
});
QUnit.test("Properties fields: m2m avatar list open chat on click", async function (assert) {
await testPropertyFieldAvatarOpenChat(assert, "many2many");
});
@@ -103,6 +103,7 @@ export class PropertyValue extends Component {
const hasAccess = many2manyValue[1] !== null;
return {
id: many2manyValue[0],
comodel: this.props.comodel,
text: hasAccess ? many2manyValue[1] : _lt("No Access"),
onClick:
hasAccess &&
@@ -19,18 +19,6 @@ input.o_field_property_input[type=number] {
}
.o_field_property_many2one_value {
img {
height: 1.4em;
width: 1.4em;
object-fit: cover;
}
&.avatar .o_input_dropdown{
// counter the avatar size to keep things aligned
margin-left: -1.6em;
padding-left: 1.6em;
}
a.disabled {
pointer-events: none;
color: $gray-400;
@@ -45,6 +33,7 @@ input.o_field_property_input[type=number] {
}
.o_delete {
line-height: 1;
margin-right: 5px;
}
.o_input_dropdown {
width: 100px !important;
@@ -61,11 +50,6 @@ input.o_field_property_input[type=number] {
}
.o_field_property_many2many_value.avatar .o_tag{
img {
margin: -0.3em;
margin-right: 0.3em;
}
.o_tag_badge_text {
padding: 0px !important;
}
@@ -66,12 +66,13 @@
</t>
<div
t-elif="props.type === 'many2one'"
t-attf-class="d-flex flex-row align-items-center o_field_property_many2one_value {{showAvatar ? 'avatar' : ''}}">
class="o_field_property_many2one_value d-flex flex-row align-items-center"
t-att-class="{'avatar': showAvatar}">
<t t-if="props.comodel">
<t t-set="hasAccess" t-value="!propertyValue || propertyValue[1]"/>
<img t-if="showAvatar and propertyValue[0] and hasAccess"
t-attf-src="/web/image/{{props.comodel}}/{{propertyValue[0]}}/avatar_128"
class="rounded-circle me-1"/>
class="rounded me-1"/>
<t t-if="props.readonly || !hasAccess">
<a
t-if="clickableRelational || !hasAccess"
+14 -18
View File
@@ -1056,25 +1056,10 @@ export class Record extends DataPoint {
const propertyFieldName = `${fieldName}.${definition.name}`;
if (propertyFieldName in this.fields) {
continue;
} else if (["many2one", "many2many"].includes(definition.type) && !definition.comodel) {
continue;
}
let widget = definition.type;
if (["many2one", "many2many"].includes(definition.type)) {
if (!definition.comodel) {
continue;
}
if (["res.users", "res.partner"].includes(definition.comodel)) {
widget =
definition.type === "many2one"
? "many2one_avatar"
: "many2many_tags_avatar";
} else {
widget = definition.type === "many2one" ? widget : "many2many_tags";
}
} else if (widget === "tags") {
widget = `property_tags`;
}
const widget = this._getWidgetFromDefinition(definition);
const parent = this.data[propertiesField.definition_record];
const relatedPropertyField = {
@@ -1124,6 +1109,17 @@ export class Record extends DataPoint {
}
}
_getWidgetFromDefinition(definition) {
if (definition.type === "many2many") {
return "many2many_tags";
} else if (definition.type === "tags") {
return "property_tags";
} else if (["res.users", "res.partner"].includes(definition.comodel)) {
return definition.type === "many2one" ? "many2one_avatar" : "many2many_tags_avatar";
}
return definition.type;
}
_createStaticList(fieldName) {
const field = this.fields[fieldName];
const activeField = this.activeFields[fieldName];