diff options
| author | Sébastien Theys <seb@odoo.com> | 2020-06-15 18:12:21 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-06-15 18:12:21 +0000 |
| commit | 3fea5b2136627c1d7e83b2a5e111952fe3eb10a1 (patch) | |
| tree | 5eefad07972a87ce4689b83260f981bc87aadad4 /addons/snailmail/static/src | |
| parent | 0a82848606151ef60eb6523e9c4c161171f214e7 (diff) | |
[REF] mail, *: refactor messaging with OWL
This commit is a significant rewriting of client-side discuss, chatter,
chat window, and messaging menu using OWL. The behavior should be broadly
the same, with some slight functional changes here and there.
From a technical standpoint, the code of messaging is mainly organized in 2
main groups of modules:
- models, which are logical entities that depict the client-side state of
messaging as a whole.
- components, which are in charge of displaying information from models.
This refactoring also introduces new JS guidelines regarding folder structure
(/static) and naming rules for JS modules.
Community PR: https://github.com/odoo/odoo/pull/39023
Enterprise PR: https://github.com/odoo/enterprise/pull/6249
Task-1914207
This PR is a collaborative work by Alexandre, Julien, Sébastien and Xavier,
with the precious help of Lucas to speed it up towards the end.
closes odoo/odoo#39023
Related: odoo/enterprise#6249
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Co-authored-by: Alexandre Kühn <aku@odoo.com>
Co-authored-by: Julien Giannone <jgi@odoo.com>
Co-authored-by: Lucas Perais <lpe@odoo.com>
Co-authored-by: Sébastien Theys <seb@odoo.com>
Co-authored-by: Xavier Dubuc <xdu@odoo.com>
Diffstat (limited to 'addons/snailmail/static/src')
23 files changed, 1588 insertions, 441 deletions
diff --git a/addons/snailmail/static/src/bugfix/bugfix.js b/addons/snailmail/static/src/bugfix/bugfix.js new file mode 100644 index 00000000000..a375f40fd3c --- /dev/null +++ b/addons/snailmail/static/src/bugfix/bugfix.js @@ -0,0 +1,10 @@ +/** + * This file allows introducing new JS modules without contaminating other files. + * This is useful when bug fixing requires adding such JS modules in stable + * versions of Odoo. Any module that is defined in this file should be isolated + * in its own file in master. + */ +odoo.define('snailmail/static/src/bugfix/bugfix.js', function (require) { +'use strict'; + +}); diff --git a/addons/snailmail/static/src/bugfix/bugfix.scss b/addons/snailmail/static/src/bugfix/bugfix.scss new file mode 100644 index 00000000000..c4272e527b4 --- /dev/null +++ b/addons/snailmail/static/src/bugfix/bugfix.scss @@ -0,0 +1,6 @@ +/** +* This file allows introducing new styles without contaminating other files. +* This is useful when bug fixing requires adding new components for instance in +* stable versions of Odoo. Any style that is defined in this file should be isolated +* in its own file in master. +*/ diff --git a/addons/snailmail/static/src/bugfix/bugfix.xml b/addons/snailmail/static/src/bugfix/bugfix.xml new file mode 100644 index 00000000000..c17906f75bb --- /dev/null +++ b/addons/snailmail/static/src/bugfix/bugfix.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + +<!-- + This file allows introducing new static templates without contaminating other files. + This is useful when bug fixing requires adding new components for instance in stable + versions of Odoo. Any template that is defined in this file should be isolated + in its own file in master. +--> + +</templates> diff --git a/addons/snailmail/static/src/bugfix/bugfix_tests.js b/addons/snailmail/static/src/bugfix/bugfix_tests.js new file mode 100644 index 00000000000..beffb29b463 --- /dev/null +++ b/addons/snailmail/static/src/bugfix/bugfix_tests.js @@ -0,0 +1,18 @@ +odoo.define('snailmail/static/src/bugfix/bugfix_tests.js', function (require) { +'use strict'; + +/** + * This file allows introducing new QUnit test modules without contaminating + * other test files. This is useful when bug fixing requires adding new + * components for instance in stable versions of Odoo. Any test that is defined + * in this file should be isolated in its own file in master. + */ +QUnit.module('snailmail', {}, function () { +QUnit.module('bugfix', {}, function () { +QUnit.module('bugfix_tests.js', { + +}); +}); +}); + +}); diff --git a/addons/snailmail/static/src/components/message/message.js b/addons/snailmail/static/src/components/message/message.js new file mode 100644 index 00000000000..1033c901367 --- /dev/null +++ b/addons/snailmail/static/src/components/message/message.js @@ -0,0 +1,87 @@ +odoo.define('snailmail/static/src/components/message/message.js', function (require) { +'use strict'; + +const components = { + Message: require('mail/static/src/components/message/message.js'), + SnailmailErrorDialog: require('snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js'), + SnailmailNotificationPopover: require('snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js'), +}; + +const { patch } = require('web.utils'); + +const { useState } = owl; + +Object.assign(components.Message.components, { + SnailmailErrorDialog: components.SnailmailErrorDialog, + SnailmailNotificationPopover: components.SnailmailNotificationPopover, +}); + +patch(components.Message, 'snailmail/static/src/components/message/message.js', { + /** + * @override + */ + _constructor() { + this._super(...arguments); + this.snailmailState = useState({ + // Determine if the error dialog is displayed. + hasDialog: false, + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _onClickFailure() { + if (this.message.message_type === 'snailmail') { + /** + * Messages from snailmail are considered to have at most one + * notification. The failure type of the whole message is considered + * to be the same as the one from that first notification, and the + * click action will depend on it. + */ + switch (this.message.notifications[0].failure_type) { + case 'sn_credit': + // URL only used in this component, not received at init + this.env.messaging.fetchSnailmailCreditsUrl(); + this.snailmailState.hasDialog = true; + break; + case 'sn_error': + this.snailmailState.hasDialog = true; + break; + case 'sn_fields': + this.message.openMissingFieldsLetterAction(); + break; + case 'sn_format': + this.message.openFormatLetterAction(); + break; + case 'sn_price': + this.snailmailState.hasDialog = true; + break; + case 'sn_trial': + // URL only used in this component, not received at init + this.env.messaging.fetchSnailmailCreditsUrlTrial(); + this.snailmailState.hasDialog = true; + break; + } + } else { + this._super(...arguments); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onDialogClosedSnailmailError() { + this.snailmailState.hasDialog = false; + }, +}); + +}); diff --git a/addons/snailmail/static/src/components/message/message.xml b/addons/snailmail/static/src/components/message/message.xml new file mode 100644 index 00000000000..d563083fc15 --- /dev/null +++ b/addons/snailmail/static/src/components/message/message.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-inherit="mail.Message" t-inherit-mode="extension"> + <xpath expr="//*[@name='failureIcon']" position="replace"> + <t t-if="message.message_type === 'snailmail'"> + <i class="o_Message_notificationIcon fa fa-paper-plane"/> + </t> + <t t-else="">$0</t> + </xpath> + + <xpath expr="//*[@name='notificationIcon']" position="replace"> + <t t-if="message.message_type === 'snailmail'"> + <i class="o_Message_notificationIcon fa fa-paper-plane"/> + </t> + <t t-else="">$0</t> + </xpath> + + <xpath expr="//*[@name='rootCondition']" position="inside"> + <t t-if="snailmailState.hasDialog"> + <SnailmailErrorDialog messageLocalId="message.localId" t-on-dialog-closed="_onDialogClosedSnailmailError"/> + </t> + </xpath> + + <!-- + It was decided that the information displayed for snailmail messages + has to be different than for standard messages, see task-1907998. + --> + <xpath expr="//NotificationPopover" position="replace"> + <t t-if="message.message_type === 'snailmail'"> + <SnailmailNotificationPopover messageLocalId="message.localId"/> + </t> + <t t-else="">$0</t> + </xpath> + </t> + +</templates> diff --git a/addons/snailmail/static/src/components/message/message_tests.js b/addons/snailmail/static/src/components/message/message_tests.js new file mode 100644 index 00000000000..10f917b2bbe --- /dev/null +++ b/addons/snailmail/static/src/components/message/message_tests.js @@ -0,0 +1,624 @@ +odoo.define('snailmail/static/src/components/message/message_tests.js', function (require) { +'use strict'; + +const components = { + Message: require('mail/static/src/components/message/message.js'), +}; +const { + afterEach: utilsAfterEach, + afterNextRender, + beforeEach: utilsBeforeEach, + start: utilsStart, +} = require('mail/static/src/utils/test_utils.js'); + +const Bus = require('web.Bus'); + +QUnit.module('snailmail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('message', {}, function () { +QUnit.module('message_tests.js', { + beforeEach() { + utilsBeforeEach(this); + + this.createMessageComponent = async message => { + const MessageComponent = components.Message; + MessageComponent.env = this.env; + this.component = new MessageComponent(null, { + messageLocalId: message.localId, + }); + delete MessageComponent.env; + await this.component.mount(this.widget.el); + }; + + this.start = async params => { + let { env, widget } = await utilsStart(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + utilsAfterEach(this); + if (this.component) { + // The component must be destroyed before the widget, because the + // widget might destroy the models before destroying the component, + // and the Message component is relying on messaging. + this.component.destroy(); + } + if (this.widget) { + this.widget.destroy(); + } + this.env = undefined; + }, +}); + +QUnit.test('Sent', async function (assert) { + assert.expect(8); + + await this.start(); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + id: 11, + notification_status: 'sent', + notification_type: 'snail', + }]], + }); + await this.createMessageComponent(message); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconContainer', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconContainer').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover', + "notification popover should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover_icon', + "popover should have one icon" + ); + assert.hasClass( + document.querySelector('.o_SnailmailNotificationPopover_icon'), + 'fa-check', + "popover should have the sent icon" + ); + assert.strictEqual( + document.querySelector('.o_SnailmailNotificationPopover').textContent.trim(), + "Sent", + "popover should have the sent text" + ); +}); + +QUnit.test('Canceled', async function (assert) { + assert.expect(8); + + await this.start(); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + id: 11, + notification_status: 'canceled', + notification_type: 'snail', + }]], + }); + await this.createMessageComponent(message); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconContainer', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconContainer').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover', + "notification popover should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover_icon', + "popover should have one icon" + ); + assert.hasClass( + document.querySelector('.o_SnailmailNotificationPopover_icon'), + 'fa-trash-o', + "popover should have the canceled icon" + ); + assert.strictEqual( + document.querySelector('.o_SnailmailNotificationPopover').textContent.trim(), + "Canceled", + "popover should have the canceled text" + ); +}); + +QUnit.test('Pending', async function (assert) { + assert.expect(8); + + await this.start(); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + id: 11, + notification_status: 'ready', + notification_type: 'snail', + }]], + }); + await this.createMessageComponent(message); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconContainer', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconContainer').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover', + "notification popover should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover_icon', + "popover should have one icon" + ); + assert.hasClass( + document.querySelector('.o_SnailmailNotificationPopover_icon'), + 'fa-clock-o', + "popover should have the pending icon" + ); + assert.strictEqual( + document.querySelector('.o_SnailmailNotificationPopover').textContent.trim(), + "Awaiting Dispatch", + "popover should have the pending text" + ); +}); + +QUnit.test('No Price Available', async function (assert) { + assert.expect(10); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'cancel_letter' && args.model === 'mail.message' && args.args[0][0] === 10) { + assert.step(args.method); + return; + } + return this._super(...arguments); + }, + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + failure_type: 'sn_price', + id: 11, + notification_status: 'exception', + notification_type: 'snail', + }]], + }); + await this.createMessageComponent(message); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconContainer', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconContainer').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog', + "error dialog should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_contentPrice', + "error dialog should have the 'no price' content" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_cancelLetterButton', + "dialog should have a 'Cancel letter' button" + ); + + await afterNextRender(() => { + document.querySelector('.o_SnailmailErrorDialog_cancelLetterButton').click(); + }); + assert.containsNone( + document.body, + '.o_SnailmailErrorDialog', + "dialog should be closed after click on 'Cancel letter'" + ); + assert.verifySteps( + ['cancel_letter'], + "should have made a RPC call to 'cancel_letter'" + ); +}); + +QUnit.test('Credit Error', async function (assert) { + assert.expect(11); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'send_letter' && args.model === 'mail.message' && args.args[0][0] === 10) { + assert.step(args.method); + return; + } + if (args.method === 'get_credits_url') { + return 'credits_url'; + } + return this._super(...arguments); + }, + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + failure_type: 'sn_credit', + id: 11, + notification_status: 'exception', + notification_type: 'snail', + }]], + }); + await this.createMessageComponent(message); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconContainer', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconContainer').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog', + "error dialog should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_contentCredit', + "error dialog should have the 'credit' content" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_resendLetterButton', + "dialog should have a 'Re-send letter' button" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_cancelLetterButton', + "dialog should have a 'Cancel letter' button" + ); + + await afterNextRender(() => { + document.querySelector('.o_SnailmailErrorDialog_resendLetterButton').click(); + }); + assert.containsNone( + document.body, + '.o_SnailmailErrorDialog', + "dialog should be closed after click on 'Re-send letter'" + ); + assert.verifySteps( + ['send_letter'], + "should have made a RPC call to 'send_letter'" + ); +}); + +QUnit.test('Trial Error', async function (assert) { + assert.expect(11); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'send_letter' && args.model === 'mail.message' && args.args[0][0] === 10) { + assert.step(args.method); + return; + } + if (args.method === 'get_credits_url') { + return 'credits_url_trial'; + } + return this._super(...arguments); + }, + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + failure_type: 'sn_trial', + id: 11, + notification_status: 'exception', + notification_type: 'snail', + }]], + }); + await this.createMessageComponent(message); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconContainer', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconContainer').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog', + "error dialog should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_contentTrial', + "error dialog should have the 'trial' content" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_resendLetterButton', + "dialog should have a 'Re-send letter' button" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_cancelLetterButton', + "dialog should have a 'Cancel letter' button" + ); + + await afterNextRender(() => { + document.querySelector('.o_SnailmailErrorDialog_resendLetterButton').click(); + }); + assert.containsNone( + document.body, + '.o_SnailmailErrorDialog', + "dialog should be closed after click on 'Re-send letter'" + ); + assert.verifySteps( + ['send_letter'], + "should have made a RPC call to 'send_letter'" + ); +}); + +QUnit.test('Format Error', async function (assert) { + assert.expect(8); + + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action, + 'snailmail.snailmail_letter_format_error_action', + "action should be the one for format error" + ); + assert.strictEqual( + payload.options.additional_context.message_id, + 10, + "action should have correct message id" + ); + }); + + await this.start({ env: { bus } }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + failure_type: 'sn_format', + id: 11, + notification_status: 'exception', + notification_type: 'snail', + }]], + }); + await this.createMessageComponent(message); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconContainer', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconContainer').click(); + }); + assert.verifySteps( + ['do_action'], + "should do an action to display the format error dialog" + ); +}); + +QUnit.test('Missing Required Fields', async function (assert) { + assert.expect(9); + + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action, + 'snailmail.snailmail_letter_missing_required_fields_action', + "action should be the one for missing fields" + ); + assert.strictEqual( + payload.options.additional_context.letter_id, + 22, // should be the same as the id returned by search + "action should have correct letter id" + ); + }); + + await this.start({ + env: { bus }, + async mockRPC(route, args) { + if (args.model === 'snailmail.letter' && args.method === 'search') { + assert.step('search'); + return [22]; // should be the same as the id given to the action + } + return this._super(...arguments); + }, + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + failure_type: 'sn_fields', + id: 11, + notification_status: 'exception', + notification_type: 'snail', + }]], + }); + await this.createMessageComponent(message); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconContainer', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconContainer').click(); + }); + assert.verifySteps( + ['search', 'do_action'], + "the id of the letter related to the message should be returned by a search and an action should be done to display the missing fields dialog" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/snailmail/static/src/components/notification_group/notification_group.js b/addons/snailmail/static/src/components/notification_group/notification_group.js new file mode 100644 index 00000000000..00cdebe15d9 --- /dev/null +++ b/addons/snailmail/static/src/components/notification_group/notification_group.js @@ -0,0 +1,27 @@ +odoo.define('snailmail/static/src/components/notification_group/notification_group.js', function (require) { +'use strict'; + +const components = { + NotificationGroup: require('mail/static/src/components/notification_group/notification_group.js'), +}; + +const { patch } = require('web.utils'); + +patch(components.NotificationGroup, 'snailmail/static/src/components/notification_group/notification_group.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + image() { + if (this.group.notification_type === 'snail') { + return '/snailmail/static/img/snailmail_failure.png'; + } + return this._super(...arguments); + }, +}); + +}); diff --git a/addons/snailmail/static/src/components/notification_group/notification_group.xml b/addons/snailmail/static/src/components/notification_group/notification_group.xml new file mode 100644 index 00000000000..7f04e716072 --- /dev/null +++ b/addons/snailmail/static/src/components/notification_group/notification_group.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-inherit="mail.NotificationGroup" t-inherit-mode="extension"> + <xpath expr="//*[hasclass('o_NotificationGroup_inlineText')]" position="inside"> + <t t-if="group.notification_type === 'snail'"> + An error occurred when sending a letter with Snailmail. + </t> + </xpath> + </t> + +</templates> diff --git a/addons/snailmail/static/src/components/notification_list/notification_list_notification_group_tests.js b/addons/snailmail/static/src/components/notification_list/notification_list_notification_group_tests.js new file mode 100644 index 00000000000..18844086473 --- /dev/null +++ b/addons/snailmail/static/src/components/notification_list/notification_list_notification_group_tests.js @@ -0,0 +1,312 @@ +odoo.define('snailmail/static/src/components/notification_list/notification_list_notification_group_tests.js', function (require) { +'use strict'; + +const components = { + NotificationList: require('mail/static/src/components/notification_list/notification_list.js'), +}; + +const { + afterEach: utilsAfterEach, + afterNextRender, + beforeEach: utilsBeforeEach, + start: utilsStart, +} = require('mail/static/src/utils/test_utils.js'); + +const Bus = require('web.Bus'); + +QUnit.module('snailmail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('notification_list', {}, function () { +QUnit.module('notification_list_notification_group_tests.js', { + beforeEach() { + utilsBeforeEach(this); + + /** + * @param {Object} param0 + * @param {string} [param0.filter='all'] + */ + this.createNotificationListComponent = async ({ filter = 'all' } = {}) => { + const NotificationListComponent = components.NotificationList; + NotificationListComponent.env = this.env; + this.component = new NotificationListComponent(null, { filter }); + await afterNextRender(() => this.component.mount(this.widget.el)); + }; + + this.start = async params => { + let { env, widget } = await utilsStart(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + utilsAfterEach(this); + if (this.component) { + this.component.destroy(); + this.component = undefined; + } + if (this.widget) { + this.widget.destroy(); + this.widget = undefined; + } + this.env = undefined; + delete components.NotificationList.env; + }, +}); + +QUnit.test('mark as read', async function (assert) { + assert.expect(6); + + this.data.initMessaging.mail_failures = [{ + date: moment.utc().format("YYYY-MM-DD HH:mm:ss"), + id: 11, + message_type: 'snailmail', + model: 'mail.channel', + notifications: [{ + failure_type: 'sn_credit', + id: 21, + notification_status: 'exception', + notification_type: 'snail', + partner_id: [41, "Someone"], + }], + res_id: 31, + res_model_name: "Channel", + }]; + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action, + 'snailmail.snailmail_letter_cancel_action', + "action should be the one to cancel letter" + ); + assert.strictEqual( + payload.options.additional_context.default_model, + 'mail.channel', + "action should have the group model as default_model" + ); + assert.strictEqual( + payload.options.additional_context.unread_counter, + 1, + "action should have the group notification length as unread_counter" + ); + }); + + await this.start({ env: { bus } }); + await this.createNotificationListComponent(); + + assert.containsOnce( + document.body, + '.o_NotificationGroup_markAsRead', + "should have 1 mark as read button" + ); + + document.querySelector('.o_NotificationGroup_markAsRead').click(); + assert.verifySteps( + ['do_action'], + "should do an action to display the cancel letter dialog" + ); +}); + +QUnit.test('notifications grouped by notification_type', async function (assert) { + assert.expect(11); + + this.data.initMessaging.mail_failures = [{ + date: moment.utc().format("YYYY-MM-DD HH:mm:ss"), + id: 11, + message_type: 'email', // key element of this test: different type + model: 'res.partner', // key element of this test: same model (and not `mail.channel``) + notifications: [{ + failure_type: 'SMTP', + id: 21, + notification_status: 'exception', + notification_type: 'email', // key element of this test: different type + partner_id: [41, "Someone"], + }], + res_id: 31, // key element of this test: same res_id + res_model_name: "Partner", + }, { + date: moment.utc().format("YYYY-MM-DD HH:mm:ss"), + id: 12, + message_type: 'snailmail', // key element of this test: different type + model: 'res.partner', // key element of this test: same model (and not `mail.channel``) + notifications: [{ + failure_type: 'sn_credit', + id: 22, + notification_status: 'exception', + notification_type: 'snail', // key element of this test: different type + partner_id: [42, "Someone else"], + }], + res_id: 31, // key element of this test: same res_id + res_model_name: "Partner", + }]; + + await this.start(); + await this.createNotificationListComponent(); + + assert.containsN( + document.body, + '.o_NotificationGroup', + 2, + "should have 2 notifications group" + ); + const groups = document.querySelectorAll('.o_NotificationGroup'); + assert.containsOnce( + groups[0], + '.o_NotificationGroup_name', + "should have 1 group name in first group" + ); + assert.strictEqual( + groups[0].querySelector('.o_NotificationGroup_name').textContent, + "Partner", + "should have model name as group name" + ); + assert.containsOnce( + groups[0], + '.o_NotificationGroup_counter', + "should have 1 group counter in first group" + ); + assert.strictEqual( + groups[0].querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(1)", + "should have 1 notification in first group" + ); + assert.strictEqual( + groups[0].querySelector('.o_NotificationGroup_inlineText').textContent.trim(), + "An error occurred when sending an email.", + "should have the group text corresponding to email" + ); + assert.containsOnce( + groups[1], + '.o_NotificationGroup_name', + "should have 1 group name in second group" + ); + assert.strictEqual( + groups[1].querySelector('.o_NotificationGroup_name').textContent, + "Partner", + "should have second model name as group name" + ); + assert.containsOnce( + groups[1], + '.o_NotificationGroup_counter', + "should have 1 group counter in second group" + ); + assert.strictEqual( + groups[1].querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(1)", + "should have 1 notification in second group" + ); + assert.strictEqual( + groups[1].querySelector('.o_NotificationGroup_inlineText').textContent.trim(), + "An error occurred when sending a letter with Snailmail.", + "should have the group text corresponding to snailmail" + ); +}); + +QUnit.test('grouped notifications by document model', async function (assert) { + // If all failures linked to a document model refers to different documents, + // a single notification should group all failures that are linked to this + // document model. + assert.expect(12); + + this.data.initMessaging.mail_failures = [{ + date: moment.utc().format("YYYY-MM-DD HH:mm:ss"), + id: 11, + message_type: 'snailmail', + model: 'res.partner', // key element of this test: same model + notifications: [{ + failure_type: 'sn_credit', + id: 21, + notification_status: 'exception', + notification_type: 'snail', + partner_id: [41, "Someone"], + }], + res_id: 31, // key element of this test: a different res_id + res_model_name: "Partner", + }, { + date: moment.utc().format("YYYY-MM-DD HH:mm:ss"), + id: 12, + message_type: 'snailmail', + model: 'res.partner', // key element of this test: same model + notifications: [{ + failure_type: 'sn_credit', + id: 22, + notification_status: 'exception', + notification_type: 'snail', + partner_id: [42, "Someone else"], + }], + res_id: 32, // key element of this test: a different res_id + res_model_name: "Partner", + }]; + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action.name, + "Snailmail Failures", + "action should have 'Snailmail Failures' as name", + ); + assert.strictEqual( + payload.action.type, + 'ir.actions.act_window', + "action should have the type act_window" + ); + assert.strictEqual( + payload.action.view_mode, + 'kanban,list,form', + "action should have 'kanban,list,form' as view_mode" + ); + assert.strictEqual( + JSON.stringify(payload.action.views), + JSON.stringify([[false, 'kanban'], [false, 'list'], [false, 'form']]), + "action should have correct views" + ); + assert.strictEqual( + payload.action.target, + 'current', + "action should have 'current' as target" + ); + assert.strictEqual( + payload.action.res_model, + 'res.partner', + "action should have the group model as res_model" + ); + assert.strictEqual( + JSON.stringify(payload.action.domain), + JSON.stringify([['message_ids.snailmail_error', '=', true]]), + "action should have 'message_has_sms_error' as domain" + ); + }); + + await this.start({ env: { bus } }); + await this.createNotificationListComponent(); + + assert.containsOnce( + document.body, + '.o_NotificationGroup', + "should have 1 notification group" + ); + assert.containsOnce( + document.body, + '.o_NotificationGroup_counter', + "should have 1 group counter" + ); + assert.strictEqual( + document.querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(2)", + "should have 2 notifications in the group" + ); + + document.querySelector('.o_NotificationGroup').click(); + assert.verifySteps( + ['do_action'], + "should do an action to display the related records" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js b/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js new file mode 100644 index 00000000000..a7daf7c1091 --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js @@ -0,0 +1,113 @@ +odoo.define('snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js', function (require) { +'use strict'; + +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const Dialog = require('web.OwlDialog'); + +const { Component } = owl; +const { useRef } = owl.hooks; + +class SnailmailErrorDialog extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useStore(props => { + const message = this.env.models['mail.message'].get(props.messageLocalId); + const notifications = message ? message.notifications : []; + return { + message: message ? message.__state : undefined, + notifications: notifications.map(notification => + notification ? notification.__state : undefined + ), + snailmail_credits_url: this.env.messaging.snailmail_credits_url, + snailmail_credits_url_trial: this.env.messaging.snailmail_credits_url_trial, + }; + }, { + compareDepth: { + notifications: 1, + }, + }); + // to manually trigger the dialog close event + this._dialogRef = useRef('dialog'); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {boolean} + */ + get hasCreditsError() { + return ( + this.notification.failure_type === 'sn_credit' || + this.notification.failure_type === 'sn_trial' + ); + } + + /** + * @returns {mail.message} + */ + get message() { + return this.env.models['mail.message'].get(this.props.messageLocalId); + } + + /** + * @returns {mail.notification} + */ + get notification() { + // Messages from snailmail are considered to have at most one notification. + return this.message.notifications[0]; + } + + /** + * @returns {string} + */ + get title() { + return this.env._t("Failed letter"); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onClickCancelLetter() { + this._dialogRef.comp._close(); + this.message.cancelLetter(); + } + + /** + * @private + */ + _onClickClose() { + this._dialogRef.comp._close(); + } + + /** + * @private + */ + _onClickResendLetter() { + this._dialogRef.comp._close(); + this.message.resendLetter(); + } + +} + +Object.assign(SnailmailErrorDialog, { + components: { Dialog }, + props: { + messageLocalId: String, + }, + template: 'snailmail.SnailmailErrorDialog', +}); + +return SnailmailErrorDialog; + +}); diff --git a/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.xml b/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.xml new file mode 100644 index 00000000000..bf4f026e6d5 --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="snailmail.SnailmailErrorDialog" owl="1"> + <Dialog contentClass="'o_SnailmailErrorDialog'" title="title" size="'medium'" t-ref="dialog"> + <t t-if="message and notification"> + <t t-if="notification.failure_type === 'sn_credit'"> + <p class="o_SnailmailErrorDialog_contentCredit"> + The letter could not be sent due to insufficient credits on your IAP account. + </p> + <t t-if="env.messaging.snailmail_credits_url"> + <div class="text-right"> + <a class="btn btn-link" t-att-href="env.messaging.snailmail_credits_url" target="_blank"> + <i class="fa fa-arrow-right"/> Buy credits + </a> + </div> + </t> + </t> + <t t-elif="notification.failure_type === 'sn_trial'"> + <p class="o_SnailmailErrorDialog_contentTrial"> + You need credits on your IAP account to send a letter. + </p> + <t t-if="env.messaging.snailmail_credits_url_trial"> + <div class="text-right"> + <a class="btn btn-link" t-att-href="env.messaging.snailmail_credits_url_trial"> + <i class="fa fa-arrow-right"/> Buy credits + </a> + </div> + </t> + </t> + <t t-elif="notification.failure_type === 'sn_price'"> + <p class="o_SnailmailErrorDialog_contentPrice"> + The country to which you want to send the letter is not supported by our service. + </p> + </t> + <t t-elif="notification.failure_type === 'sn_error'"> + <p class="o_SnailmailErrorDialog_contentError"> + An unknown error occurred. Please contact our <a href="https://www.odoo.com/help" target="new">support</a> for further assistance. + </p> + </t> + + <t t-set-slot="buttons"> + <t t-if="hasCreditsError"> + <button class="o_SnailmailErrorDialog_resendLetterButton btn btn-primary" t-on-click="_onClickResendLetter">Re-send letter</button> + </t> + <button class="o_SnailmailErrorDialog_cancelLetterButton btn" + t-att-class="{ + 'btn-primary': !hasCreditsError, + 'btn-secondary': hasCreditsError, + }" + t-on-click="_onClickCancelLetter" + > + Cancel letter + </button> + <button class="o_SnailmailErrorDialog_closeButton btn btn-secondary" t-on-click="_onClickClose">Close</button> + </t> + </t> + </Dialog> + </t> + +</templates> diff --git a/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js new file mode 100644 index 00000000000..0845ce07818 --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js @@ -0,0 +1,86 @@ +odoo.define('snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js', function (require) { +'use strict'; + +const { Component } = owl; +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +class SnailmailNotificationPopover extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useStore(props => { + const message = this.env.models['mail.message'].get(props.messageLocalId); + const notifications = message ? message.notifications : []; + return { + message: message ? message.__state : undefined, + notifications: notifications.map(notification => notification ? notification.__state : undefined), + }; + }, { + compareDepth: { + notifications: 1, + }, + }); + } + + /** + * @returns {string} + */ + get iconClass() { + switch (this.notification.notification_status) { + case 'sent': + return 'fa fa-check'; + case 'ready': + return 'fa fa-clock-o'; + case 'canceled': + return 'fa fa-trash-o'; + default: + return 'fa fa-exclamation text-danger'; + } + } + + /** + * @returns {string} + */ + get iconTitle() { + switch (this.notification.notification_status) { + case 'sent': + return this.env._t("Sent"); + case 'ready': + return this.env._t("Awaiting Dispatch"); + case 'canceled': + return this.env._t("Canceled"); + default: + return this.env._t("Error"); + } + } + + /** + * @returns {mail.message} + */ + get message() { + return this.env.models['mail.message'].get(this.props.messageLocalId); + } + + /** + * @returns {mail.notification} + */ + get notification() { + // Messages from snailmail are considered to have at most one notification. + return this.message.notifications[0]; + } + +} + +Object.assign(SnailmailNotificationPopover, { + props: { + messageLocalId: String, + }, + template: 'snailmail.SnailmailNotificationPopover', +}); + +return SnailmailNotificationPopover; + +}); diff --git a/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.scss b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.scss new file mode 100644 index 00000000000..970bc79d997 --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.scss @@ -0,0 +1,7 @@ +// ----------------------------------------------------------------------------- +// Layout +// ----------------------------------------------------------------------------- + +.o_SnailmailNotificationPopover_icon { + margin-inline-end: map-get($spacers, 2); +} diff --git a/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.xml b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.xml new file mode 100644 index 00000000000..873b08f4d37 --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="snailmail.SnailmailNotificationPopover" owl="1"> + <div class="o_SnailmailNotificationPopover"> + <t t-if="message and notification"> + <i class="o_SnailmailNotificationPopover_icon" t-att-class="iconClass" role="img"/> + <span t-esc="iconTitle"/> + </t> + </div> + </t> + +</templates> diff --git a/addons/snailmail/static/src/js/mail_failure.js b/addons/snailmail/static/src/js/mail_failure.js deleted file mode 100644 index 903633c42ff..00000000000 --- a/addons/snailmail/static/src/js/mail_failure.js +++ /dev/null @@ -1,51 +0,0 @@ -odoo.define('snailmail.model.MailFailure', function (require) { -"use strict"; - -var MailFailure = require('mail.model.MailFailure'); -var core = require('web.core'); -var _t = core._t; - -MailFailure.include({ - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - getPreview: function () { - var preview = this._super.apply(this, arguments); - if (this._messageType === 'snailmail') { - _.extend(preview, { - id: 'snailmail_failure', - }); - } - return preview; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _getPreviewBody() { - if (this._messageType === 'snailmail') { - return _t("An error occured when sending a letter with Snailmail."); - } - return this._super(...arguments); - }, - /** - * @override - */ - _getPreviewImage() { - if (this._messageType === 'snailmail') { - return '/snailmail/static/img/snailmail_failure.png'; - } - return this._super(...arguments); - }, -}); - -}); diff --git a/addons/snailmail/static/src/js/message.js b/addons/snailmail/static/src/js/message.js deleted file mode 100644 index 624f2b4027f..00000000000 --- a/addons/snailmail/static/src/js/message.js +++ /dev/null @@ -1,48 +0,0 @@ -odoo.define('snailmail.model.Message', function (require) { -"use strict"; - -var Message = require('mail.model.Message'); - -Message.include({ - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Cancels the 'snailmail.letter' which has a message_id corresponding to - * the ID of this message. - * - * @returns {Deferred} - */ - cancelLetter() { - return this._rpc({ - model: 'mail.message', - method: 'cancel_letter', - args: [[this.getID()]], - }); - }, - /** - * @override - */ - getNotificationIcon() { - if (this.getType() === 'snail') { - return 'fa fa-paper-plane'; - } - return this._super(...arguments); - }, - /** - * Retries to send the 'snailmail.letter' corresponding to this message - * - * @returns {Deferred} - */ - resendLetter() { - return this._rpc({ - model: 'mail.message', - method: 'send_letter', - args: [[this.getID()]], - }); - }, -}); - -}); diff --git a/addons/snailmail/static/src/js/systray_messaging_menu.js b/addons/snailmail/static/src/js/systray_messaging_menu.js deleted file mode 100644 index 2265df23ca0..00000000000 --- a/addons/snailmail/static/src/js/systray_messaging_menu.js +++ /dev/null @@ -1,78 +0,0 @@ -odoo.define('snailmail.systray.MessagingMenu', function (require) { -"use strict"; - -var MessagingMenu = require('mail.systray.MessagingMenu'); - -MessagingMenu.include({ - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Called when clicking on a preview related to a snailmail failure - * - * @private - * @param {$.Element} $target DOM of preview element clicked - */ - _clickSnailmailFailurePreview: function ($target) { - var documentID = $target.data('document-id'); - var documentModel = $target.data('document-model'); - if (documentModel && documentID) { - this._openDocument(documentModel, documentID); - } else if (documentModel !== 'mail.channel') { - // preview of mail failures grouped to different document of same model - this.do_action({ - name: "Snailmail failures", - type: 'ir.actions.act_window', - view_mode: 'kanban,list,form', - views: [[false, 'kanban'], [false, 'list'], [false, 'form']], - target: 'current', - res_model: documentModel, - domain: [['message_ids.snailmail_error', '=', true]], - }); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - * @override - */ - _onClickPreview: function (ev) { - var $target = $(ev.currentTarget); - var previewID = $target.data('preview-id'); - - if (previewID === 'snailmail_failure') { - this._clickSnailmailFailurePreview($target); - } else { - this._super.apply(this, arguments); - } - }, - /** - * @private - * @override - */ - _onClickPreviewMarkAsRead: function (ev) { - ev.stopPropagation(); - var $preview = $(ev.currentTarget).closest('.o_mail_preview'); - var previewID = $preview.data('preview-id'); - if (previewID === 'snailmail_failure') { - var documentModel = $preview.data('document-model'); - var unreadCounter = $preview.data('unread-counter'); - this.do_action('snailmail.snailmail_letter_cancel_action', { - additional_context: { - default_model: documentModel, - unread_counter: unreadCounter - } - }); - } else { - this._super.apply(this, arguments); - } - }, -}); - -}); diff --git a/addons/snailmail/static/src/js/thread_widget.js b/addons/snailmail/static/src/js/thread_widget.js deleted file mode 100644 index 84ab7f9a877..00000000000 --- a/addons/snailmail/static/src/js/thread_widget.js +++ /dev/null @@ -1,215 +0,0 @@ -odoo.define('snailmail.widget.Thread', function (require) { -"use strict"; - -var ThreadWidget = require('mail.widget.Thread'); -var Dialog = require('web.Dialog'); - -var core = require('web.core'); -var _t = core._t; - -ThreadWidget.include({ - - /** - * @override - */ - render: function (thread, options) { - this._super.apply(this, arguments); - var messages = _.clone(thread.getMessages({ domain: options.domain || [] })); - this._messages = messages; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - * @param {integer} messageID - * @param {string} content - */ - _openCreditErrorDialog: function (messageID, content) { - var message = _.find(this._messages, function (message) { - return message.getID() === messageID; - }); - new Dialog(this, { - size: 'medium', - title: _t("Failed letter"), - $content: $('<div>').html(content), - buttons: [{ - text: _t("Re-send letter"), - classes: 'btn-primary', - close: true, - click: (...args) => message.resendLetter(...args), - }, { - text: _t("Cancel letter"), - close: true, - click: (...args) => message.cancelLetter(...args), - }, { - text: _t("Close"), - close: true, - }], - }).open(); - }, - /** - * @private - * @param {integer} messageID - * @param {string} content - */ - _openGenericErrorDialog: function (messageID, content) { - var message = _.find(this._messages, function (message) { - return message.getID() === messageID; - }); - new Dialog(this, { - size: 'medium', - title: _t("Failed letter"), - $content: $('<div>').html(content), - buttons: [{ - text: _t("Cancel letter"), - classes: 'btn-primary', - close: true, - click: (...args) => message.cancelLetter(...args), - }, { - text: _t("Close"), - close: true, - }], - }).open(); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - * @param {MouseEvent} event - */ - _onClickCreditError: function (ev) { - var self = this; - var messageID = $(ev.currentTarget).data('message-id'); - this._rpc({ - model: 'iap.account', - method: 'get_credits_url', - args: ['snailmail'], - }).then(function (link) { - var content = _.str.sprintf(_t( - '<p>The letter could not be sent due to insufficient credits on your IAP account.</p>' + - '<div class= "text-right">' + - '<a class="btn btn-link buy_credits" href=%s target="_blank">' + - '<i class= "fa fa-arrow-right"/> Buy credits' + - '</a>' + - '</div>' - ), link); - self._openCreditErrorDialog(messageID, content); - }); - }, - /** - * @private - * @param {MouseEvent} event - */ - _onClickFormatError: function (ev) { - var messageID = $(ev.currentTarget).data('message-id'); - this.do_action('snailmail.snailmail_letter_format_error_action', { - additional_context: { - message_id: messageID, - } - }); - }, - /** - * Discards the generic error handler since more specific handlers are - * defined here. - * - * @override - */ - _onClickMessageNotificationError(ev) { - const $target = $(ev.currentTarget); - if ($target.data('message-type') === 'snailmail') { - if ($target.data('failure-type') === 'sn_fields') { - this._onClickMissingRequiredFields(ev); - } - if ($target.data('failure-type') === 'sn_credit') { - this._onClickCreditError(ev); - } - if ($target.data('failure-type') === 'sn_trial') { - this._onClickTrialError(ev); - } - if ($target.data('failure-type') === 'sn_price') { - this._onClickNoPriceAvailable(ev); - } - if ($target.data('failure-type') === 'sn_format') { - this._onClickFormatError(ev); - } - if ($target.data('failure-type') === 'sn_error') { - this._onClickUnknownError(ev); - } - } else { - this._super(...arguments); - } - }, - /** - * @private - * @param {MouseEvent} event - */ - _onClickMissingRequiredFields: function (ev) { - var self = this; - - var messageID = $(ev.currentTarget).data('message-id'); - var domain = [['message_id', '=', messageID]]; - this._rpc({ - model: 'snailmail.letter', - method: 'search', - args: [domain], - }).then(function (letterIds) { - self.do_action('snailmail.snailmail_letter_missing_required_fields_action', { - additional_context: { - letter_id: letterIds[0] - }, - on_close: function () { - self.trigger_up('reload'); - } - }); - }); - }, - /** - * @private - * @param {MouseEvent} event - */ - _onClickNoPriceAvailable: function (ev) { - var messageID = $(ev.currentTarget).data('message-id'); - var content = _t('<p>The country to which you want to send the letter is not supported by our service.</p>'); - this._openGenericErrorDialog(messageID, content); - }, - /** - * @private - * @param {MouseEvent} event - */ - _onClickTrialError: function (ev) { - var self = this; - var messageID = $(ev.currentTarget).data('message-id'); - this._rpc({ - model: 'iap.account', - method: 'get_credits_url', - args: ['snailmail', '', 0, true], - }).then(function (link) { - var content = _.str.sprintf(_t( - '<p>You need credits on your IAP account to send a letter.</p>' + - '<div class= "text-right">' + - '<a class="btn btn-link buy_credits" href=%s>' + - '<i class= "fa fa-arrow-right"/> Buy credits' + - '</a>' + - '</div>' - ), link); - self._openCreditErrorDialog(messageID, content); - }); - }, - /** - * @private - * @param {MouseEvent} event - */ - _onClickUnknownError: function (ev) { - var messageID = $(ev.currentTarget).data('message-id'); - var content = _t('<p>An unknown error occured. Please contact our <a href="https://www.odoo.com/help" target="new">support</a> for further assistance.</p>'); - this._openGenericErrorDialog(messageID, content); - }, -}); - -}); diff --git a/addons/snailmail/static/src/models/message/message.js b/addons/snailmail/static/src/models/message/message.js new file mode 100644 index 00000000000..2ac25fc1ecf --- /dev/null +++ b/addons/snailmail/static/src/models/message/message.js @@ -0,0 +1,69 @@ +odoo.define('snailmail/static/src/models/message.message.js', function (require) { +'use strict'; + +const { registerInstancePatchModel } = require('mail/static/src/model/model_core.js'); + +registerInstancePatchModel('mail.message', 'snailmail/static/src/models/message.message.js', { + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * Cancels the 'snailmail.letter' corresponding to this message. + * + * @returns {Deferred} + */ + async cancelLetter() { + // the result will come from longpolling: message_notification_update + await this.async(() => this.env.services.rpc({ + model: 'mail.message', + method: 'cancel_letter', + args: [[this.id]], + })); + }, + /** + * Opens the action about 'snailmail.letter' format error. + */ + openFormatLetterAction() { + this.env.bus.trigger('do-action', { + action: 'snailmail.snailmail_letter_format_error_action', + options: { + additional_context: { + message_id: this.id, + }, + }, + }); + }, + /** + * Opens the action about 'snailmail.letter' missing fields. + */ + async openMissingFieldsLetterAction() { + const letterIds = await this.async(() => this.env.services.rpc({ + model: 'snailmail.letter', + method: 'search', + args: [[['message_id', '=', this.id]]], + })); + this.env.bus.trigger('do-action', { + action: 'snailmail.snailmail_letter_missing_required_fields_action', + options: { + additional_context: { + letter_id: letterIds[0], + }, + }, + }); + }, + /** + * Retries to send the 'snailmail.letter' corresponding to this message. + */ + async resendLetter() { + // the result will come from longpolling: message_notification_update + await this.async(() => this.env.services.rpc({ + model: 'mail.message', + method: 'send_letter', + args: [[this.id]], + })); + }, +}); + +}); diff --git a/addons/snailmail/static/src/models/messaging/messaging.js b/addons/snailmail/static/src/models/messaging/messaging.js new file mode 100644 index 00000000000..f00d0544dff --- /dev/null +++ b/addons/snailmail/static/src/models/messaging/messaging.js @@ -0,0 +1,38 @@ +odoo.define('snailmail/static/src/models/messaging/messaging.js', function (require) { +'use strict'; + +const { + registerInstancePatchModel, + registerFieldPatchModel, +} = require('mail/static/src/model/model_core.js'); +const { attr } = require('mail/static/src/model/model_field.js'); + +registerInstancePatchModel('mail.messaging', 'snailmail/static/src/models/messaging/messaging.js', { + async fetchSnailmailCreditsUrl() { + const snailmail_credits_url = await this.async(() => this.env.services.rpc({ + model: 'iap.account', + method: 'get_credits_url', + args: ['snailmail'], + })); + this.update({ + snailmail_credits_url, + }); + }, + async fetchSnailmailCreditsUrlTrial() { + const snailmail_credits_url_trial = await this.async(() => this.env.services.rpc({ + model: 'iap.account', + method: 'get_credits_url', + args: ['snailmail', '', 0, true], + })); + this.update({ + snailmail_credits_url_trial, + }); + }, +}); + +registerFieldPatchModel('mail.messaging', 'snailmail/static/src/models/messaging/messaging.js', { + snailmail_credits_url: attr(), + snailmail_credits_url_trial: attr(), +}); + +}); diff --git a/addons/snailmail/static/src/models/notification_group/notification_group.js b/addons/snailmail/static/src/models/notification_group/notification_group.js new file mode 100644 index 00000000000..c63ebe4c066 --- /dev/null +++ b/addons/snailmail/static/src/models/notification_group/notification_group.js @@ -0,0 +1,57 @@ +odoo.define('snailmail/static/src/models/notification_group/notification_group.js', function (require) { +'use strict'; + +const { + registerInstancePatchModel, +} = require('mail/static/src/model/model_core.js'); + +registerInstancePatchModel('mail.notification_group', 'snailmail/static/src/models/notification_group/notification_group.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + openCancelAction() { + if (this.notification_type === 'snail') { + this.env.bus.trigger('do-action', { + action: 'snailmail.snailmail_letter_cancel_action', + options: { + additional_context: { + default_model: this.res_model, + unread_counter: this.notifications.length, + }, + }, + }); + } + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _openDocuments() { + if (this.notification_type === 'snail') { + this.env.bus.trigger('do-action', { + action: { + name: this.env._t("Snailmail Failures"), + type: 'ir.actions.act_window', + view_mode: 'kanban,list,form', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], + target: 'current', + res_model: this.res_model, + domain: [['message_ids.snailmail_error', '=', true]], + }, + }); + } + return this._super(...arguments); + }, +}); + +}); diff --git a/addons/snailmail/static/src/xml/thread.xml b/addons/snailmail/static/src/xml/thread.xml deleted file mode 100644 index df3ae1e08d4..00000000000 --- a/addons/snailmail/static/src/xml/thread.xml +++ /dev/null @@ -1,49 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<template id="template" xml:space="preserve"> - - <t t-inherit="mail.widget.Thread.Message" t-inherit-mode="extension"> - <xpath expr="//span[@name='notification_icon']" position="attributes"> - <!-- - Messages from snaimail are considered to have at most one notification. - The failure type of the whole message is considered to be the same as the one from - that first notification, and the click action will depend on it. - --> - <attribute name="t-att-data-failure-type">message.getNotifications()[0].failure_type</attribute> - </xpath> - </t> - - <t t-inherit="mail.widget.Thread.Message.MailTooltip" t-inherit-mode="extension"> - <!-- - It was decided that the information displayed for snaimail messages has to be - different than for standard messages, see task-1907998. - --> - <xpath expr="//span[@name='notification_status']" position="replace"> - <t t-if="notification.notification_type === 'snail'"> - <span class="d-inline-block text-center o_thread_tooltip_icon"> - <t t-if="notification.notification_status === 'sent'"> - <i class="fa fa-check o_thread_tooltip_snailmail_icon" title="Sent" role="img" aria-label="Sent"/> - Sent - </t> - <t t-elif="notification.notification_status === 'canceled'"> - <i class="fa fa-trash-o o_thread_tooltip_snailmail_icon" title="Canceled" role="img" aria-label="Canceled"/> - Canceled - </t> - <t t-elif="notification.notification_status === 'ready'"> - <i class="fa fa-clock-o o_thread_tooltip_snailmail_icon" title="Awaiting Dispatch" role="img" aria-label="Awaiting Dispatch"/> - Awaiting Dispatch - </t> - <t t-else=""> - <i class="fa fa-exclamation text-danger o_thread_tooltip_snailmail_icon" title="Error" role="img" aria-label="Error"/> - Error - </t> - </span> - </t> - <t t-else="">$0</t> - </xpath> - <xpath expr="//span[@name='partner_name']" position="replace"> - <t t-if="notification.notification_type === 'snail'"></t> - <t t-else="">$0</t> - </xpath> - </t> - -</template> |
