diff --git a/addons/account/static/src/js/tours/account.js b/addons/account/static/src/js/tours/account.js
index 2e79bd1f234..010a156432a 100644
--- a/addons/account/static/src/js/tours/account.js
+++ b/addons/account/static/src/js/tours/account.js
@@ -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
diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py
index 017874d6a4e..1fd5aa1a154 100644
--- a/addons/mail/__manifest__.py
+++ b/addons/mail/__manifest__.py
@@ -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',
],
},
diff --git a/addons/mail/static/src/fields/many2many_tags_email.js b/addons/mail/static/src/fields/many2many_tags_email.js
new file mode 100644
index 00000000000..7750798b4df
--- /dev/null
+++ b/addons/mail/static/src/fields/many2many_tags_email.js
@@ -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);
diff --git a/addons/mail/static/src/fields/many2many_tags_email.xml b/addons/mail/static/src/fields/many2many_tags_email.xml
new file mode 100644
index 00000000000..e4f29c7f063
--- /dev/null
+++ b/addons/mail/static/src/fields/many2many_tags_email.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js
deleted file mode 100644
index de1af974445..00000000000
--- a/addons/mail/static/src/js/many2many_tags_email.js
+++ /dev/null
@@ -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