[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:
@@ -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
|
||||
|
||||
@@ -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('@') < 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 && 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);
|
||||
@@ -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('@') < 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",
|
||||
|
||||
Reference in New Issue
Block a user