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} 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); diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index 050562b08bf..1d905770b0c 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -8,13 +8,4 @@ - - - -
- - -
-
-
diff --git a/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js b/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js index 8bbf2e66309..4be51d91a1a 100644 --- a/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js @@ -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", diff --git a/addons/project/static/tests/tours/project_sharing_tour.js b/addons/project/static/tests/tours/project_sharing_tour.js index dbd3cf50ded..c66fb7b2e2f 100644 --- a/addons/project/static/tests/tours/project_sharing_tour.js +++ b/addons/project/static/tests/tours/project_sharing_tour.js @@ -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')); diff --git a/addons/website_sale/static/tests/tours/website_sale_shop_mail.js b/addons/website_sale/static/tests/tours/website_sale_shop_mail.js index d1f2bdbb6ea..61828a72fb3 100644 --- a/addons/website_sale/static/tests/tours/website_sale_shop_mail.js +++ b/addons/website_sale/static/tests/tours/website_sale_shop_mail.js @@ -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', }, { diff --git a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js index fd8d9e01ae2..acca7e88f2a 100644 --- a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js +++ b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js @@ -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",