[IMP] mail: Migrate many2many_tags_email widget to Owl

closes odoo/odoo#99230

Related: odoo/enterprise#31254
Signed-off-by: William Braeckman (wbr) <wbr@odoo.com>
This commit is contained in:
William Braeckman
2022-09-13 13:53:08 +02:00
parent 77f3953e1a
commit e2ba23ea17
10 changed files with 132 additions and 153 deletions
@@ -96,7 +96,7 @@ tour.register('account_tour', {
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: _t("Send the invoice and check what the customer will receive."),
}, {
trigger: "input[name=email]",
trigger: ".o_field_widget[name=email] input, input[name=email]",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
+2
View File
@@ -190,6 +190,7 @@ 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/utils/*.js',
'mail/static/src/scss/*.scss',
'mail/static/src/component_hooks/*.js',
@@ -224,6 +225,7 @@ For more specific needs, you may also assign custom-defined actions
],
'web.assets_qweb': [
'mail/static/src/xml/*.xml',
'mail/static/src/fields/*.xml',
'mail/static/src/components/*/*.xml',
],
},
@@ -0,0 +1,104 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useOpenMany2XRecord } from "@web/views/fields/relational_utils";
import { sprintf } from "@web/core/utils/strings";
import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
import { TagsList } from "@web/views/fields/many2many_tags/tags_list";
const { onMounted, onWillUpdateProps } = owl;
export class FieldMany2ManyTagsEmailTagsList extends TagsList {}
FieldMany2ManyTagsEmailTagsList.template = "FieldMany2ManyTagsEmailTagsList";
export class FieldMany2ManyTagsEmail extends Many2ManyTagsField {
setup() {
super.setup();
this.openedDialogs = 0;
this.recordsIdsToAdd = [];
this.openMany2xRecord = useOpenMany2XRecord({
resModel: this.props.relation,
activeActions: {
canCreate: false,
canCreateEdit: false,
canWrite: true,
},
isToMany: true,
onRecordSaved: async (record) => {
if (record.data.email) {
this.recordsIdsToAdd.push(record.resId);
}
},
fieldString: this.props.string,
});
// Using onWillStart causes an infinite loop, onMounted will handle the initial
// check and onWillUpdateProps handles any addition to the field.
onMounted(this.checkEmails.bind(this, this.props));
onWillUpdateProps(this.checkEmails.bind(this));
}
async checkEmails(props) {
const invalidRecords = props.value.records.filter((record) => !record.data.email);
// Remove records with invalid data, open form view to edit those and readd them if they are updated correctly.
const dialogDefs = [];
for (const record of invalidRecords) {
dialogDefs.push(this.openMany2xRecord({
resId: record.resId,
context: props.record.getFieldContext(this.props.name),
title: sprintf(this.env._t("Edit: %s"), record.data.display_name),
}));
}
this.openedDialogs += invalidRecords.length;
const invalidRecordIds = invalidRecords.map(rec => rec.resId);
if (invalidRecordIds.length) {
this.props.value.replaceWith(props.value.currentIds.filter(id => !invalidRecordIds.includes(id)));
}
return Promise.all(dialogDefs).then(() => {
this.openedDialogs -= invalidRecords.length;
if (this.openedDialogs || !this.recordsIdsToAdd.length) {
return;
}
props.value.add(this.recordsIdsToAdd, { isM2M: true });
this.recordsIdsToAdd = [];
});
}
get tags() {
// Add email to our tags
const tags = super.tags;
const emailByResId = this.props.value.records.reduce((acc, record) => {
acc[record.resId] = record.data.email;
return acc;
}, {});
tags.forEach(tag => tag.email = emailByResId[tag.resId]);
return tags;
}
};
FieldMany2ManyTagsEmail.components = {
...FieldMany2ManyTagsEmail.components,
TagsList: FieldMany2ManyTagsEmailTagsList,
};
FieldMany2ManyTagsEmail.fieldsToFetch = Object.assign({},
Many2ManyTagsField.fieldsToFetch,
{email: {name: 'email', type: 'char'}}
);
registry.category("fields").add("many2many_tags_email", FieldMany2ManyTagsEmail);
/* fieldsToFetch are retrieved from legacy widget.. */
import field_registry from 'web.field_registry';
import relational_fields from 'web.relational_fields';
var M2MTags = relational_fields.FieldMany2ManyTags;
var FieldMany2ManyTagsEmailLegacy = M2MTags.extend({
fieldsToFetch: _.extend({}, M2MTags.prototype.fieldsToFetch, {
email: {type: 'char'},
}),
});
field_registry.add('many2many_tags_email', FieldMany2ManyTagsEmailLegacy);
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="FieldMany2ManyTagsEmailTagsList" t-inherit="web.TagsList" t-inherit-mode="primary" owl="1">
<xpath expr="//span[contains(@t-attf-class, 'badge')]" position="replace">
<div t-if="tag.email" t-attf-class="badge rounded-pill dropdown o_tag_color_0 #{tag.email.indexOf('@') &lt; 0 ? 'o_tag_error' : ''}" t-att-data-color="tag.colorIndex" t-att-data-index="tag_index" t-att-data-id="tag.id" t-att-title="tag.text">
<span class="o_badge_text" t-att-title="tag.email"><t t-esc="tag.text"/></span>
<a t-if="!readonly &amp;&amp; tag.onDelete" t-on-click.stop.prevent="tag.onDelete" href="#" class="fa fa-times o_delete" title="Delete" aria-label="Delete"/>
</div>
</xpath>
</t>
</templates>
@@ -1,130 +0,0 @@
/** @odoo-module **/
import BasicModel from 'web.BasicModel';
import form_common from 'web.view_dialogs';
import field_registry from 'web.field_registry';
import relational_fields from 'web.relational_fields';
var M2MTags = relational_fields.FieldMany2ManyTags;
BasicModel.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} record - an element from the localData
* @param {string} fieldName
* @return {Promise<Object>} the promise is resolved with the
* invalidPartnerIds
*/
_setInvalidMany2ManyTagsEmail: function (record, fieldName) {
var self = this;
var localID = (record._changes && fieldName in record._changes) ?
record._changes[fieldName] :
record.data[fieldName];
var list = this._applyX2ManyOperations(this.localData[localID]);
var invalidPartnerIds = [];
_.each(list.data, function (id) {
var record = self.localData[id];
if (!record.data.email) {
invalidPartnerIds.push(record);
}
});
var def;
if (invalidPartnerIds.length) {
// remove invalid partners
var changes = {operation: 'DELETE', ids: _.pluck(invalidPartnerIds, 'id')};
def = this._applyX2ManyChange(record, fieldName, changes);
}
return Promise.resolve(def).then(function () {
return {
invalidPartnerIds: _.pluck(invalidPartnerIds, 'res_id'),
};
});
},
});
var FieldMany2ManyTagsEmail = M2MTags.extend({
tag_template: "FieldMany2ManyTagsEmail",
fieldsToFetch: _.extend({}, M2MTags.prototype.fieldsToFetch, {
email: {type: 'char'},
}),
specialData: "_setInvalidMany2ManyTagsEmail",
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Open a popup for each invalid partners (without email) to fill the email.
*
* @private
* @returns {Promise}
*/
_checkEmailPopup: function () {
var self = this;
var popupDefs = [];
var validPartners = [];
// propose the user to correct invalid partners
_.each(this.record.specialData[this.name].invalidPartnerIds, function (resID) {
var def = new Promise(function (resolve, reject) {
var pop = new form_common.FormViewDialog(self, {
res_model: self.field.relation,
res_id: resID,
context: self.record.context,
title: "",
on_saved: function (record) {
if (record.data.email) {
validPartners.push(record.res_id);
}
},
}).open();
pop.on('closed', self, function () {
resolve();
});
});
popupDefs.push(def);
});
return Promise.all(popupDefs).then(function() {
// All popups have been processed for the given ids
// It is now time to set the final value with valid partners ids.
validPartners = _.uniq(validPartners);
if (validPartners.length) {
var values = _.map(validPartners, function (id) {
return {id: id};
});
self._setValue({
operation: 'ADD_M2M',
ids: values,
});
}
});
},
/**
* Override to check if all many2many values have an email set before
* rendering the widget.
*
* @override
* @private
*/
_render: function () {
var self = this;
var _super = this._super.bind(this);
return new Promise(function (resolve, reject) {
if (self.record.specialData[self.name].invalidPartnerIds.length) {
resolve(self._checkEmailPopup());
} else {
resolve();
}
}).then(function () {
return _super.apply(self, arguments);
});
},
});
field_registry.add('many2many_tags_email', FieldMany2ManyTagsEmail);
-9
View File
@@ -8,13 +8,4 @@
</span>
</t>
</div>
<t t-name="FieldMany2ManyTagsEmail" t-extend="FieldMany2ManyTag">
<t t-jquery="[t-attf-class*=badge]" t-operation="replace">
<div t-attf-class="badge rounded-pill dropdown o_tag_color_0 #{el.email.indexOf('@') &lt; 0 ? 'o_tag_error' : ''}" t-att-data-color="color" t-att-data-index="el_index" t-att-data-id="el.id" t-attf-title="Tag color: #{colornames[color]}">
<span class="o_badge_text" t-att-title="el.email"><t t-esc="el.display_name"/></span>
<a t-if="!readonly" href="#" class="fa fa-times o_delete" title="Delete" aria-label="Delete"/>
</div>
</t>
</t>
</templates>
@@ -420,6 +420,8 @@ QUnit.test('fieldmany2many tags email (edition)', async function (assert) {
if (args.method === 'read' && args.model === 'res.partner') {
assert.step(JSON.stringify(args.args[0]));
assert.ok(args.args[1].includes('email'), "should read the email");
} else if (args.method === "get_formview_id") {
return false;
}
},
});
@@ -435,27 +437,26 @@ QUnit.test('fieldmany2many tags email (edition)', async function (assert) {
);
assert.verifySteps([`[${resPartnerId1}]`]);
assert.containsOnce(document.body, '.o_field_many2manytags[name="partner_ids"] .badge.o_tag_color_0',
assert.containsOnce(document.body, '.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0',
"should contain one tag");
// add an other existing tag
await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
await testUtils.fields.many2one.searchAndClickItem('partner_ids', { search: 'silver' });
await selectDropdownItem(document.body, 'partner_ids', "silver");
assert.strictEqual($('.modal-body.o_act_window').length, 1,
assert.strictEqual(document.querySelectorAll('.modal-content.o_form_view_dialog').length, 1,
"there should be one modal opened to edit the empty email");
assert.strictEqual($('.modal-body.o_act_window input[name="name"]').val(), "silver",
assert.strictEqual(document.querySelector(".modal-content.o_form_view_dialog .o_input#name").value, "silver",
"the opened modal in edit mode should be a form view dialog with the res.partner 14");
assert.strictEqual($('.modal-body.o_act_window input[name="email"]').length, 1,
assert.strictEqual(document.querySelectorAll(".modal-content.o_form_view_dialog .o_input#email").length, 1,
"there should be an email field in the modal");
// set the email and save the modal (will rerender the form view)
await testUtils.fields.editInput($('.modal-body.o_act_window input[name="email"]'), 'coucou@petite.perruche');
await testUtils.dom.click($('.modal-footer .btn-primary'));
await testUtils.fields.editInput($('.modal-content.o_form_view_dialog .o_input#email'), 'coucou@petite.perruche');
await testUtils.dom.click($('.modal-content.o_form_view_dialog .o_form_button_save'));
assert.containsN(document.body, '.o_field_many2manytags[name="partner_ids"] .badge.o_tag_color_0', 2,
assert.containsN(document.body, '.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0', 2,
"should contain the second tag");
const firstTag = document.querySelector('.o_field_many2manytags[name="partner_ids"] .badge.o_tag_color_0');
const firstTag = document.querySelector('.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0');
assert.strictEqual(firstTag.querySelector('.o_badge_text').innerText, "gold",
"tag should only show name");
assert.hasAttrValue(firstTag.querySelector('.o_badge_text'), 'title', "coucou@petite.perruche",
@@ -12,7 +12,7 @@ const projectSharingSteps = [...tour.stepUtils.goToAppSteps("project.menu_main_p
trigger: 'div.o_field_radio[name="access_mode"] div.o_radio_item > input[data-value="edit"]',
content: 'Select "Edit" as Access mode in the "Share Project" wizard.',
}, {
trigger: '.o_field_many2one[name="partner_ids"]',
trigger: '.o_field_many2many_tags_email[name=partner_ids]',
content: 'Select the user portal as collaborator to the "Project Sharing" project.',
run: function (actions) {
actions.text('Georges', this.$anchor.find('input'));
@@ -68,7 +68,7 @@ tour.register('shop_mail', {
},
{
content: "Open recipients dropdown",
trigger: '.o_field_many2one[name="partner_ids"] .ui-autocomplete-input',
trigger: '.o_field_many2many_tags_email[name=partner_ids] input',
run: 'click',
},
{
@@ -600,7 +600,7 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
},
...tour.stepUtils.statusbarButtonsSteps('Send by Email', _t("Try to send it to email"), ".o_statusbar_status .btn-primary:contains('Quotation')"),
{
trigger: ".o_field_widget[name=email]",
trigger: ".o_field_widget[name=email] input",
content: _t("Enter an email address"),
position: "right",
run: "text test@the_flow.com",