[IMP] mail,hr: migrate m2x_avatar_user and m2x_avatar_employee to wowl2

Those fields may be used in the new views.

Some of the legacy fields had to be kept because they are used
in some legacy views, more notably the gantt and activity views as well
as in the mrp work order tablet mode.

X-original-commit: 1c871ad9ea4a77468911ff6a5546688a26398a8b
Part-of: odoo/odoo#100764
This commit is contained in:
William Braeckman
2022-09-22 12:23:27 +02:00
parent 7909827571
commit 18426a62eb
21 changed files with 860 additions and 81 deletions
+1
View File
@@ -66,6 +66,7 @@
'web.qunit_suite_tests': [
'hr/static/tests/helpers/*.js',
'hr/static/tests/m2x_avatar_employee_tests.js',
'hr/static/tests/m2x_avatar_employee_legacy_tests.js',
'hr/static/tests/standalone_m2o_avatar_employee_tests.js',
],
'web.assets_tests': [
@@ -4,9 +4,7 @@ import fieldRegistry from 'web.field_registry';
import {
Many2OneAvatarUser,
KanbanMany2OneAvatarUser,
KanbanMany2ManyAvatarUser,
ListMany2ManyAvatarUser,
Many2ManyAvatarUser,
} from '@mail/js/m2x_avatar_user';
@@ -44,10 +42,8 @@ const M2XAvatarEmployeeMixin = {
};
export const Many2OneAvatarEmployee = Many2OneAvatarUser.extend(M2XAvatarEmployeeMixin);
export const KanbanMany2OneAvatarEmployee = KanbanMany2OneAvatarUser.extend(M2XAvatarEmployeeMixin);
fieldRegistry.add('many2one_avatar_employee', Many2OneAvatarEmployee);
fieldRegistry.add('kanban.many2one_avatar_employee', KanbanMany2OneAvatarEmployee);
const M2MAvatarEmployeeMixin = Object.assign(M2XAvatarEmployeeMixin, {
//----------------------------------------------------------------------
@@ -63,11 +59,8 @@ export const Many2ManyAvatarEmployee = Many2ManyAvatarUser.extend(M2MAvatarEmplo
export const KanbanMany2ManyAvatarEmployee = KanbanMany2ManyAvatarUser.extend(M2MAvatarEmployeeMixin, {});
export const ListMany2ManyAvatarEmployee = ListMany2ManyAvatarUser.extend(M2MAvatarEmployeeMixin, {});
fieldRegistry.add('many2many_avatar_employee', Many2ManyAvatarEmployee);
fieldRegistry.add('kanban.many2many_avatar_employee', KanbanMany2ManyAvatarEmployee);
fieldRegistry.add('list.many2many_avatar_employee', ListMany2ManyAvatarEmployee);
export default {
Many2OneAvatarEmployee,
@@ -0,0 +1,15 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Many2ManyTagsAvatarUserField, KanbanMany2ManyTagsAvatarUserField } from "@mail/views/fields/many2many_avatar_user_field/many2many_avatar_user_field";
export class Many2ManyTagsAvatarEmployeeField extends Many2ManyTagsAvatarUserField {}
Many2ManyTagsAvatarEmployeeField.additionalClasses = [...Many2ManyTagsAvatarUserField.additionalClasses, "o_field_many2many_avatar_user"];
registry.category("fields").add("many2many_avatar_employee", Many2ManyTagsAvatarEmployeeField);
export class KanbanMany2ManyTagsAvatarEmployeeField extends KanbanMany2ManyTagsAvatarUserField {}
KanbanMany2ManyTagsAvatarEmployeeField.additionalClasses = [...KanbanMany2ManyTagsAvatarUserField.additionalClasses, "o_field_many2many_avatar_user"];
registry.category("fields").add("kanban.many2many_avatar_employee", KanbanMany2ManyTagsAvatarEmployeeField);
registry.category("fields").add("list.many2many_avatar_employee", KanbanMany2ManyTagsAvatarEmployeeField);
@@ -0,0 +1,13 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Many2OneAvatarUserField, KanbanMany2OneAvatarUserField } from "@mail/views/fields/many2one_avatar_user_field/many2one_avatar_user_field";
export class Many2OneAvatarEmployeeField extends Many2OneAvatarUserField {}
Many2OneAvatarEmployeeField.additionalClasses = [...Many2OneAvatarUserField.additionalClasses, "o_field_many2one_avatar_user"];
registry.category("fields").add("many2one_avatar_employee", Many2OneAvatarEmployeeField);
export class KanbanMany2OneAvatarEmployeeField extends KanbanMany2OneAvatarUserField {}
registry.category("fields").add("kanban.many2one_avatar_employee", KanbanMany2OneAvatarEmployeeField);
@@ -0,0 +1,14 @@
/** @odoo-module **/
import { helpers } from "@mail/views/open_chat_hook";
import { patch } from "@web/core/utils/patch";
patch(helpers, "hr_m2x_avatar_employee", {
SUPPORTED_M2X_AVATAR_MODELS: [...helpers.SUPPORTED_M2X_AVATAR_MODELS, "hr.employee", "hr.employee.public"],
buildOpenChatParams: function (resModel, id) {
if (["hr.employee", "hr.employee.public"].includes(resModel)) {
return { employeeId: id };
}
return this._super(...arguments);
},
});
@@ -0,0 +1,182 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
import { Many2OneAvatarEmployee } from '@hr/js/m2x_avatar_employee';
import { dom } from 'web.test_utils';
QUnit.module('hr', {}, function () {
QUnit.module('M2XAvatarEmployeeLegacy', {
beforeEach() {
Many2OneAvatarEmployee.prototype.partnerIds = {};
},
});
QUnit.test('many2one_avatar_employee: click on an employee not associated with a user', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const hrEmployeePublicId1 = pyEnv['hr.employee.public'].create({ name: 'Mario' });
const m2xHrAvatarUserId1 = pyEnv['m2x.avatar.employee'].create({ employee_id: hrEmployeePublicId1 });
const views = {
'm2x.avatar.employee,false,form': '<form js_class="legacy_form"><field name="employee_id" widget="many2one_avatar_employee"/></form>',
};
const { openView } = await start({
mockRPC(route, args) {
if (args.method === 'read') {
assert.step(`read ${args.model} ${args.args[0]}`);
}
},
serverData: { views },
services: {
notification: makeFakeNotificationService(message => {
assert.ok(
true,
"should display a toast notification after failing to open chat"
);
assert.strictEqual(
message,
"You can only chat with employees that have a dedicated user.",
"should display the correct information in the notification"
);
}),
},
});
await openView({
res_model: 'm2x.avatar.employee',
res_id: m2xHrAvatarUserId1,
views: [[false, 'form']],
});
assert.strictEqual(document.querySelector('.o_field_widget[name=employee_id]').innerText.trim(), 'Mario');
await dom.click(document.querySelector('.o_m2o_avatar > img'));
assert.verifySteps([
`read m2x.avatar.employee ${m2xHrAvatarUserId1}`,
`read hr.employee.public ${hrEmployeePublicId1}`,
]);
});
QUnit.test('many2many_avatar_employee widget in form view', async function (assert) {
assert.expect(8);
const pyEnv = await startServer();
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{}, {}]);
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{}, {}]);
const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([
{ user_id: resUsersId1, user_partner_id: resPartnerId1 },
{ user_id: resUsersId2, user_partner_id: resPartnerId2 },
]);
const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create(
{ employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] },
);
const views = {
'm2x.avatar.employee,false,form': '<form js_class="legacy_form"><field name="employee_ids" widget="many2many_avatar_employee"/></form>',
};
const { openView } = await start({
mockRPC(route, args) {
if (args.method === 'read') {
assert.step(`read ${args.model} ${args.args[0]}`);
}
},
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.employee',
res_id: m2xAvatarEmployeeId1,
views: [[false, 'form']],
});
assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
"should have 2 records");
assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'),
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
"should have correct avatar image");
await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar'));
await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]);
assert.verifySteps([
`read m2x.avatar.employee ${m2xAvatarEmployeeId1}`,
`read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`,
`read hr.employee.public ${hrEmployeePublicId1}`,
`read hr.employee.public ${hrEmployeePublicId2}`,
]);
assert.containsN(
document.body,
'.o_ChatWindowHeader_name',
2,
"should have 2 chat windows"
);
});
QUnit.test('many2many_avatar_employee: click on an employee not associated with a user', async function (assert) {
assert.expect(10);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const resUsersId1 = pyEnv['res.users'].create({});
const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([
{},
{ user_id: resUsersId1, user_partner_id: resPartnerId1 },
]);
const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create(
{ employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] },
);
const views = {
'm2x.avatar.employee,false,form': '<form js_class="legacy_form"><field name="employee_ids" widget="many2many_avatar_employee"/></form>',
};
const { openView } = await start({
mockRPC(route, args) {
if (args.method === 'read') {
assert.step(`read ${args.model} ${args.args[0]}`);
}
},
serverData: { views },
services: {
notification: makeFakeNotificationService(message => {
assert.ok(
true,
"should display a toast notification after failing to open chat"
);
assert.strictEqual(
message,
"You can only chat with employees that have a dedicated user.",
"should display the correct information in the notification"
);
}),
},
});
await openView({
res_model: 'm2x.avatar.employee',
res_id: m2xAvatarEmployeeId1,
views: [[false, 'form']],
});
assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
"should have 2 records");
assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'),
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
"should have correct avatar image");
await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar'));
await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]);
assert.verifySteps([
`read m2x.avatar.employee ${hrEmployeePublicId1}`,
`read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`,
`read hr.employee.public ${hrEmployeePublicId1}`,
`read hr.employee.public ${hrEmployeePublicId2}`
]);
assert.containsOnce(document.body, '.o_ChatWindowHeader_name',
"should have 1 chat window");
});
});
@@ -55,9 +55,9 @@ QUnit.module('hr', {}, function () {
views: [[false, 'list']],
});
assert.strictEqual(document.querySelector('.o_data_cell span').innerText, 'Mario');
assert.strictEqual(document.querySelectorAll('.o_data_cell span')[1].innerText, 'Luigi');
assert.strictEqual(document.querySelectorAll('.o_data_cell span')[2].innerText, 'Mario');
assert.strictEqual(document.querySelector('.o_data_cell span:not(.o_m2o_avatar)').innerText, 'Mario');
assert.strictEqual(document.querySelectorAll('.o_data_cell span:not(.o_m2o_avatar)')[1].innerText, 'Luigi');
assert.strictEqual(document.querySelectorAll('.o_data_cell span:not(.o_m2o_avatar)')[2].innerText, 'Mario');
// click on first employee
await afterNextRender(() =>
@@ -223,14 +223,14 @@ QUnit.module('hr', {}, function () {
res_id: m2xAvatarEmployeeId1,
views: [[false, 'form']],
});
assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
assert.containsN(document.body, '.o_field_many2many_avatar_employee .badge', 2,
"should have 2 records");
assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'),
assert.strictEqual(document.querySelector('.o_field_many2many_avatar_employee .badge img').getAttribute('data-src'),
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
"should have correct avatar image");
await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar'));
await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]);
await dom.click(document.querySelector('.o_field_many2many_avatar_employee .badge .o_m2m_avatar'));
await dom.click(document.querySelectorAll('.o_field_many2many_avatar_employee .badge .o_m2m_avatar')[1]);
assert.verifySteps([
`read m2x.avatar.employee ${m2xAvatarEmployeeId1}`,
@@ -279,7 +279,7 @@ QUnit.module('hr', {}, function () {
res_model: 'm2x.avatar.employee',
views: [[false, 'list']],
});
assert.containsN(document.body, '.o_data_cell:first .o_field_many2manytags > span', 2,
assert.containsN(document.body, '.o_data_cell:first .o_field_many2many_avatar_employee > div > span', 2,
"should have two avatar");
// click on first employee badge
@@ -366,12 +366,12 @@ QUnit.module('hr', {}, function () {
res_model: 'm2x.avatar.employee',
views: [[false, 'kanban']],
});
assert.containsN(document.body, '.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar', 2,
assert.containsN(document.body, '.o_kanban_record:first .o_field_many2many_avatar_employee img.o_m2m_avatar', 2,
"should have 2 avatar images");
assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2manytags img.o_m2m_avatar').getAttribute('data-src'),
assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar').getAttribute('data-src'),
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
"should have correct avatar image");
assert.strictEqual(document.querySelectorAll('.o_kanban_record .o_field_many2manytags img.o_m2m_avatar')[1].getAttribute('data-src'),
assert.strictEqual(document.querySelectorAll('.o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar')[1].getAttribute('data-src'),
`/web/image/hr.employee.public/${hrEmployeePublicId2}/avatar_128`,
"should have correct avatar image");
@@ -428,14 +428,14 @@ QUnit.module('hr', {}, function () {
views: [[false, 'form']],
});
assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
assert.containsN(document.body, '.o_field_many2many_avatar_employee .badge', 2,
"should have 2 records");
assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'),
assert.strictEqual(document.querySelector('.o_field_many2many_avatar_employee .badge img').getAttribute('data-src'),
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
"should have correct avatar image");
await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar'));
await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]);
await dom.click(document.querySelector('.o_field_many2many_avatar_employee .badge .o_m2m_avatar'));
await dom.click(document.querySelectorAll('.o_field_many2many_avatar_employee .badge .o_m2m_avatar')[1]);
assert.verifySteps([
`read m2x.avatar.employee ${hrEmployeePublicId1}`,
+2 -2
View File
@@ -190,8 +190,6 @@ For more specific needs, you may also assign custom-defined actions
# defines mixins and variables used by multiple components
'mail/static/src/components/notification_list/notification_list_item.scss',
'mail/static/src/js/**/*.js',
'mail/static/src/fields/*.js',
'mail/static/src/fields/*.xml',
'mail/static/src/utils/*.js',
'mail/static/src/scss/*.scss',
'mail/static/src/xml/*.xml',
@@ -202,6 +200,8 @@ For more specific needs, you may also assign custom-defined actions
('include', 'mail.assets_messaging'),
'mail/static/src/services/*.js',
'mail/static/src/views/**/*.js',
'mail/static/src/views/**/*.xml',
'mail/static/src/views/**/*.scss',
'mail/static/src/webclient/commands/*.js',
'mail/static/src/widgets/*/*.js',
'mail/static/src/widgets/*/*.scss',
+9 -15
View File
@@ -2,7 +2,7 @@
import core from 'web.core';
import fieldRegistry from 'web.field_registry';
import { FieldMany2ManyTagsAvatar, KanbanMany2ManyTagsAvatar, ListMany2ManyTagsAvatar, Many2OneAvatar } from 'web.relational_fields';
import { FieldMany2ManyTagsAvatar, Many2OneAvatar, KanbanMany2ManyTagsAvatar } from 'web.relational_fields';
import session from 'web.session';
const { Component } = owl;
@@ -191,15 +191,6 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
}
});
export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({
_template: 'mail.KanbanMany2OneAvatarUser',
init() {
this._super(...arguments);
this.displayAvatarName = this.nodeOptions.display_avatar_name || false;
},
});
const M2MAvatarMixin = Object.assign(M2XAvatarMixin, {
events: Object.assign({}, FieldMany2ManyTagsAvatar.prototype.events, {
'click .o_m2m_avatar': '_onAvatarClicked',
@@ -327,13 +318,16 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
}
});
export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({
_template: 'mail.KanbanMany2OneAvatarUser',
init() {
this._super(...arguments);
this.displayAvatarName = this.nodeOptions.display_avatar_name || false;
},
});
export const KanbanMany2ManyAvatarUser = KanbanMany2ManyTagsAvatar.extend(M2MAvatarMixin, {});
export const ListMany2ManyAvatarUser = ListMany2ManyTagsAvatar.extend(M2MAvatarMixin, {});
fieldRegistry.add('many2one_avatar_user', Many2OneAvatarUser);
fieldRegistry.add('kanban.many2one_avatar_user', KanbanMany2OneAvatarUser);
fieldRegistry.add('activity.many2one_avatar_user', KanbanMany2OneAvatarUser);
fieldRegistry.add('many2many_avatar_user', Many2ManyAvatarUser);
fieldRegistry.add('kanban.many2many_avatar_user', KanbanMany2ManyAvatarUser);
fieldRegistry.add('list.many2many_avatar_user', ListMany2ManyAvatarUser);
@@ -3,12 +3,14 @@
filter: brightness(0.8);
}
.o_field_many2one_avatar_user,
.o_field_many2one_avatar.o_clickable_m2x_avatar {
.o_m2o_avatar:hover {
@include o-avatar-image-hover;
}
}
.o_field_many2many_avatar_user,
.o_field_many2manytags.avatar.o_clickable_m2x_avatar {
img.o_m2m_avatar {
margin-right: 2px;
@@ -0,0 +1,117 @@
/** @odoo-module **/
import { useCommand } from "@web/core/commands/command_hook";
import { useService } from "@web/core/utils/hooks";
import { Domain } from "@web/core/domain";
const { useComponent, useEnv } = owl;
/**
* Use this hook to add "Assign to.." and "Assign/Unassign me" to the command palette.
*/
export function useAssignUserCommand() {
const component = useComponent();
const env = useEnv();
const orm = useService("orm");
const user = useService("user");
if (
component.props.relation !== "res.users" ||
component.props.record.activeFields[component.props.name].viewType !== "form"
) {
return;
}
const getCurrentIds = () => {
if (component.props.type === "many2one" && component.props.value) {
return [component.props.value[0]];
} else if (component.props.type === "many2many") {
return component.props.value.currentIds;
}
return [];
};
const add = async (record) => {
if (component.props.type === "many2one") {
component.props.update(record);
} else if (component.props.type === "many2many") {
component.props.update({
operation: "REPLACE_WITH",
resIds: [...getCurrentIds(), record[0]],
});
}
};
const remove = async (record) => {
if (component.props.type === "many2one") {
component.props.update([]);
} else if (component.props.type === "many2many") {
component.props.update({
operation: "REPLACE_WITH",
resIds: getCurrentIds().filter((id) => id !== record[0]),
});
}
};
const provide = async (env, options) => {
const value = options.searchValue.trim();
let domain = component.props.record.getFieldDomain(component.props.name);
const context = component.props.record.getFieldContext(component.props.name);
if (component.props.type === "many2many") {
const selectedUserIds = getCurrentIds();
if (selectedUserIds.length) {
domain = Domain.and([domain, [["id", "not in", selectedUserIds]]]);
}
}
const searchResult = await orm.call(component.props.relation, "name_search", [], {
name: value,
args: domain.toList(),
operator: "ilike",
limit: 80,
context,
});
return searchResult.map((record) => ({
name: record[1],
action: add.bind(null, record),
}));
};
useCommand(
env._t("Assign to ..."),
() => ({
configByNameSpace: {
default: {
emptyMessage: env._t("No users found"),
},
},
placeholder: env._t("Select a user..."),
providers: [
{
provide,
},
],
}),
{
category: "smart_action",
hotkey: "alt+i",
global: true,
}
);
useCommand(
env._t("Assign/Unassign to me"),
() => {
const record = [user.userId, user.name];
if (getCurrentIds().includes(user.userId)) {
remove(record);
} else {
add(record);
}
},
{
category: "smart_action",
hotkey: "alt+shift+i",
global: true,
}
);
}
@@ -0,0 +1,69 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { TagsList } from "@web/views/fields/many2many_tags/tags_list";
import {
Many2ManyTagsAvatarField,
ListKanbanMany2ManyTagsAvatarField,
} from "@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field";
import { useOpenChat } from "@mail/views/open_chat_hook";
import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hook";
export class Many2ManyAvatarUserTagsList extends TagsList {}
Many2ManyAvatarUserTagsList.template = "mail.Many2ManyAvatarUserTagsList";
export class Many2ManyTagsAvatarUserField extends Many2ManyTagsAvatarField {
setup() {
super.setup();
this.openChat = useOpenChat(this.props.relation);
useAssignUserCommand();
}
get tags() {
return super.tags.map((tag) => ({
...tag,
onImageClicked: () => {
this.openChat(tag.resId);
},
}));
}
}
Many2ManyTagsAvatarUserField.components = {
...Many2ManyTagsAvatarField.components,
TagsList: Many2ManyAvatarUserTagsList,
};
Many2ManyTagsAvatarUserField.additionalClasses = ["o_field_many2many_tags_avatar"];
registry.category("fields").add("many2many_avatar_user", Many2ManyTagsAvatarUserField);
export class KanbanMany2ManyTagsAvatarUserField extends ListKanbanMany2ManyTagsAvatarField {
setup() {
super.setup();
this.openChat = useOpenChat(this.props.relation);
useAssignUserCommand();
}
get displayText() {
const isList = this.props.record.activeFields[this.props.name].viewType === "list";
return (isList && this.props.value.records.length === 1) || !this.props.readonly;
}
get tags() {
const recordFromId = (id) => this.props.value.records.find((rec) => rec.id === id);
return super.tags.map((tag) => ({
...tag,
onImageClicked: () => {
this.openChat(recordFromId(tag.id).resId);
},
}));
}
}
KanbanMany2ManyTagsAvatarUserField.template = "mail.KanbanMany2ManyTagsAvatarUserField";
KanbanMany2ManyTagsAvatarUserField.components = {
...ListKanbanMany2ManyTagsAvatarField.components,
TagsList: Many2ManyAvatarUserTagsList,
};
KanbanMany2ManyTagsAvatarUserField.additionalClasses = ["o_field_many2many_tags_avatar"];
registry.category("fields").add("kanban.many2many_avatar_user", KanbanMany2ManyTagsAvatarUserField);
registry.category("fields").add("list.many2many_avatar_user", KanbanMany2ManyTagsAvatarUserField);
@@ -0,0 +1,37 @@
.o_kanban_renderer, .o_list_renderer {
.o_field_many2many_avatar_user {
&:not(:has(.o_tags_input)) {
display: flex !important;
}
.o_field_tags:not(.o_tags_input) {
.o_tag {
background-color: transparent;
box-shadow: none;
color: inherit;
margin-right: 0px;
}
.o_m2m_avatar, .o_m2m_avatar_empty {
width: 20px;
height: 20px;
margin-left: 0px;
}
.o_m2m_avatar_empty {
background-color: $o-gray-300;
vertical-align: bottom;
}
}
}
}
.o_kanban_renderer {
.o_field_many2many_avatar_user {
.o_field_tags {
.o_m2m_avatar, .o_m2m_avatar_empty {
margin-right: 0px;
}
}
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="mail.Many2ManyAvatarUserTagsList" t-inherit="web.TagsList" t-inherit-mode="primary" owl="1">
<img position="attributes">
<attribute name="t-on-click.stop.prevent">tag.onImageClicked</attribute>
</img>
</t>
<t t-name="mail.KanbanMany2ManyTagsAvatarUserField" t-inherit="web.Many2ManyTagsAvatarField" t-inherit-mode="primary" owl="1">
<TagsList position="attributes">
<attribute name="displayBadge">!props.readonly</attribute>
<attribute name="displayText">displayText</attribute>
</TagsList>
</t>
</templates>
@@ -0,0 +1,46 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Many2OneAvatarField } from "@web/views/fields/many2one_avatar/many2one_avatar_field";
import { useOpenChat } from "@mail/views/open_chat_hook";
import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hook";
export class Many2OneAvatarUserField extends Many2OneAvatarField {
setup() {
super.setup();
this.openChat = useOpenChat(this.props.relation);
useAssignUserCommand();
}
onClickAvatar() {
this.openChat(this.props.value[0]);
}
}
Many2OneAvatarUserField.template = "mail.Many2OneAvatarUserField";
Many2OneAvatarUserField.additionalClasses = ["o_field_many2one_avatar"];
registry.category("fields").add("many2one_avatar_user", Many2OneAvatarUserField);
export class KanbanMany2OneAvatarUserField extends Many2OneAvatarUserField {
/**
* All props are normally passed to the Many2OneField however since
* we add a new one, we need to filter it out.
*/
get m2oFieldProps() {
return Object.fromEntries(Object.entries(this.props).filter(([key, _val]) => key in Many2OneAvatarField.props));
}
}
KanbanMany2OneAvatarUserField.template = "mail.KanbanMany2OneAvatarUserField";
KanbanMany2OneAvatarUserField.props = {
...Many2OneAvatarUserField.props,
displayAvatarName: { type: Boolean, optional: true },
};
KanbanMany2OneAvatarUserField.extractProps = ({ attrs, field }) => {
return {
...Many2OneAvatarUserField.extractProps({ attrs, field }),
displayAvatarName: attrs.options.display_avatar_name || false,
};
};
registry.category("fields").add("kanban.many2one_avatar_user", KanbanMany2OneAvatarUserField);
registry.category("fields").add("activity.many2one_avatar_user", KanbanMany2OneAvatarUserField);
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="mail.Many2OneAvatarUserField" t-inherit="web.Many2OneAvatarField" t-inherit-mode="primary" owl="1">
<xpath expr="//span[hasclass('o_m2o_avatar')]" position="attributes">
<attribute name="t-on-click.stop.prevent">onClickAvatar</attribute>
</xpath>
</t>
<t t-name="mail.KanbanMany2OneAvatarUserField" t-inherit="mail.Many2OneAvatarUserField" t-inherit-mode="primary" owl="1">
<Many2OneField position="attributes">
<attribute name="t-if">props.readonly and props.displayAvatarName</attribute>
<attribute name="t-props">m2oFieldProps</attribute>
</Many2OneField>
</t>
</templates>
@@ -0,0 +1,27 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
export const helpers = {
SUPPORTED_M2X_AVATAR_MODELS: ["res.users"],
buildOpenChatParams: (resModel, id) => {
if (resModel === "res.users") {
return { userId: id };
}
},
}
export function useOpenChat(resModel) {
const messagingService = useService("messaging");
if (!helpers.SUPPORTED_M2X_AVATAR_MODELS.includes(resModel)) {
throw new Error(
`This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(
helpers.SUPPORTED_M2X_AVATAR_MODELS
)}`
);
}
return async (id) => {
const messaging = await messagingService.get();
messaging.openChat(helpers.buildOpenChatParams(resModel, id));
};
}
@@ -0,0 +1,253 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { click, getFixture, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
import { registry } from "@web/core/registry";
import { makeLegacyCommandService } from "@web/legacy/utils";
import core from 'web.core';
import session from 'web.session';
import makeTestEnvironment from "web.test_env";
import { dom, nextTick } from 'web.test_utils';
let target;
QUnit.module('mail', {}, function () {
QUnit.module('M2XAvatarUserLegacy', {
beforeEach() {
target = getFixture();
},
});
QUnit.test('many2many_avatar_user widget in form view', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' });
const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 });
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] });
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
res_id: m2xAvatarUserId1,
views: [[false, 'form']],
});
await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar'));
assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened');
assert.strictEqual(
document.querySelector('.o_ChatWindowHeader_name').textContent,
'Partner 1',
'First chat window should be related to partner 1'
);
});
QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1] = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_id" widget="many2one_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario")
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Your Company, Mitchell Admin",
"Public user",
"Mario",
"Luigi",
"Yoshi",
])
await click(target, "#o_command_3")
await legacyExtraNextTick();
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi")
});
QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{ name: "Mario" }, { name: "Luigi" }]);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
patchWithCleanup(session, { user_id: [resUsersId2] });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_id" widget="many2one_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario")
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I")
assert.ok(idx >= 0);
// Assign me (Luigi)
triggerHotkey("alt+shift+i")
await legacyExtraNextTick();
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi")
// Unassign me
triggerHotkey("control+k");
await nextTick();
await click([...target.querySelectorAll(".o_command")][idx])
await legacyExtraNextTick();
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "")
});
QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId2] });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Your Company, Mitchell Admin",
"Public user",
"Luigi"
]);
await click(target, "#o_command_2");
await legacyExtraNextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent);
assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]);
});
QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to me"', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const [resUsersId1, resUsersId2, resUsersId3] = pyEnv['res.users'].create(
[{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId3] });
patchWithCleanup(session, { user_id: [resUsersId2] });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({ serverData: { views } });
await openView({
res_id: m2xAvatarUserId1,
type: 'ir.actions.act_window',
target: 'current',
res_model: 'm2x.avatar.user',
'view_mode': 'form',
'views': [[false, 'form']],
});
let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
triggerHotkey("control+k");
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I");
assert.ok(idx >= 0);
// Assign me (Luigi)
triggerHotkey("alt+shift+i");
await legacyExtraNextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]);
// Unassign me
triggerHotkey("control+k");
await nextTick();
await click([...target.querySelectorAll(".o_command")][idx]);
await legacyExtraNextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
});
QUnit.test('avatar_user widget displays the appropriate user image in form view', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" });
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] });
const views = {
'm2x.avatar.user,false,form': '<form js_class="legacy_form"><field name="user_ids" widget="many2many_avatar_user"/></form>',
};
const { openView } = await start({
serverData: { views },
});
await openView({
res_model: 'm2x.avatar.user',
res_id: m2xAvatarUserId1,
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'),
`/web/image/res.users/${resUsersId1}/avatar_128`,
'Should have correct avatar image'
);
});
});
@@ -1,22 +1,18 @@
/** @odoo-module **/
import { Many2OneAvatarUser } from '@mail/js/m2x_avatar_user';
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { click, getFixture, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
import { click, getFixture, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
import { registry } from "@web/core/registry";
import { makeLegacyCommandService } from "@web/legacy/utils";
import core from 'web.core';
import session from 'web.session';
import makeTestEnvironment from "web.test_env";
import { session } from "@web/session";
import { dom, nextTick } from 'web.test_utils';
import { popoverService } from "@web/core/popover/popover_service";
import { tooltipService } from "@web/core/tooltip/tooltip_service";
let target;
QUnit.module('mail', {}, function () {
QUnit.module('M2XAvatarUser', {
beforeEach() {
// reset the cache before each test
Many2OneAvatarUser.prototype.partnerIds = {};
target = getFixture();
},
});
@@ -67,7 +63,7 @@ QUnit.module('mail', {}, function () {
views: [[false, 'form']],
});
await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar'));
await dom.click(document.querySelector('.o_field_many2many_avatar_user .badge .o_m2m_avatar'));
assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened');
assert.strictEqual(
document.querySelector('.o_ChatWindowHeader_name').textContent,
@@ -84,6 +80,8 @@ QUnit.module('mail', {}, function () {
[{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }, { name: "Tapu" }],
);
pyEnv['m2x.avatar.user'].create({ user_ids: resUsersIds });
registry.category("services").add("popover", popoverService);
registry.category("services").add("tooltip", tooltipService);
const views = {
'm2x.avatar.user,false,kanban':
`<kanban>
@@ -111,17 +109,17 @@ QUnit.module('mail', {}, function () {
views: [[false, 'kanban']],
});
assert.containsOnce(document.body, '.o_kanban_record .o_field_many2manytags .o_m2m_avatar_empty',
assert.containsOnce(document.body, '.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty',
"should have o_m2m_avatar_empty span");
assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2manytags .o_m2m_avatar_empty').innerText.trim(), "+2",
assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty').innerText.trim(), "+2",
"should have +2 in o_m2m_avatar_empty");
document.querySelector('.o_kanban_record .o_field_many2manytags .o_m2m_avatar_empty').dispatchEvent(new Event('mouseover'));
document.querySelector('.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty').dispatchEvent(new Event('mouseenter'));
await nextTick();
assert.containsOnce(document.body, '.popover',
"should open a popover hover on o_m2m_avatar_empty");
assert.strictEqual(document.querySelector('.popover .popover-body > div').innerText.trim(), 'Luigi', 'should have a right text in popover');
assert.strictEqual(document.querySelectorAll('.popover .popover-body > div')[1].innerText.trim(), 'Tapu', 'should have a right text in popover');
assert.strictEqual(document.querySelector('.popover .o-tooltip > div').innerText.trim(), 'Luigi', 'should have a right text in popover');
assert.strictEqual(document.querySelectorAll('.popover .o-tooltip > div')[1].innerText.trim(), 'Tapu', 'should have a right text in popover');
});
QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) {
@@ -132,9 +130,6 @@ QUnit.module('mail', {}, function () {
[{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_id" widget="many2one_avatar_user"/></form>',
@@ -148,7 +143,7 @@ QUnit.module('mail', {}, function () {
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario")
assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user > div > a").textContent, "Mario")
triggerHotkey("control+k")
await nextTick();
@@ -165,8 +160,8 @@ QUnit.module('mail', {}, function () {
"Yoshi",
])
await click(target, "#o_command_3")
await legacyExtraNextTick();
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi")
await nextTick();
assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user > div > a").textContent, "Luigi")
});
QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) {
@@ -175,10 +170,7 @@ QUnit.module('mail', {}, function () {
const pyEnv = await startServer();
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{ name: "Mario" }, { name: "Luigi" }]);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 });
patchWithCleanup(session, { user_id: [resUsersId2] });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" });
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_id" widget="many2one_avatar_user"/></form>',
@@ -192,23 +184,23 @@ QUnit.module('mail', {}, function () {
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario")
assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user > div > a").textContent, "Mario")
triggerHotkey("control+k")
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I")
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/Unassign to meALT + SHIFT + I")
assert.ok(idx >= 0);
// Assign me (Luigi)
triggerHotkey("alt+shift+i")
await legacyExtraNextTick();
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi")
await nextTick();
assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user > div > a").textContent, "Luigi")
// Unassign me
triggerHotkey("control+k");
await nextTick();
await click([...target.querySelectorAll(".o_command")][idx])
await legacyExtraNextTick();
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "")
await nextTick();
assert.containsNone(target, ".o_field_many2one_avatar_user > div > a");
});
QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) {
@@ -219,9 +211,6 @@ QUnit.module('mail', {}, function () {
[{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId2] });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
@@ -252,7 +241,7 @@ QUnit.module('mail', {}, function () {
]);
await click(target, "#o_command_2");
await legacyExtraNextTick();
await nextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent);
assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]);
});
@@ -265,10 +254,7 @@ QUnit.module('mail', {}, function () {
[{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }],
);
const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId3] });
patchWithCleanup(session, { user_id: [resUsersId2] });
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" });
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
@@ -287,12 +273,12 @@ QUnit.module('mail', {}, function () {
triggerHotkey("control+k");
await nextTick();
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I");
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/Unassign to meALT + SHIFT + I");
assert.ok(idx >= 0);
// Assign me (Luigi)
triggerHotkey("alt+shift+i");
await legacyExtraNextTick();
await nextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]);
@@ -300,7 +286,7 @@ QUnit.module('mail', {}, function () {
triggerHotkey("control+k");
await nextTick();
await click([...target.querySelectorAll(".o_command")][idx]);
await legacyExtraNextTick();
await nextTick();
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
});
@@ -380,7 +366,7 @@ QUnit.module('mail', {}, function () {
views: [[false, 'form']],
});
assert.strictEqual(
document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'),
document.querySelector('.o_field_many2many_avatar_user.o_field_widget .badge img').getAttribute('data-src'),
`/web/image/res.users/${resUsersId1}/avatar_128`,
'Should have correct avatar image'
);