[IMP] mail, snailmail: convert legacy dialog with t-ref to model

In preparation to using Owl 2 in discuss code.

task-2738648

closes odoo/odoo#83598

Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-02-02 19:50:45 +00:00
committed by Sébastien Theys
parent ef9429231c
commit 0df4b0029b
37 changed files with 599 additions and 424 deletions
@@ -22,7 +22,6 @@
<AttachmentList
className="'o_attachmentBox_attachmentList'"
localId="attachmentBoxView.attachmentList.localId"
onAttachmentRemoved="attachmentBoxView.onAttachmentRemoved"
/>
</t>
<button class="o_AttachmentBox_buttonAdd btn btn-link" type="button" t-on-click="attachmentBoxView.onClickAddAttachment">
@@ -277,7 +277,7 @@ QUnit.test('remove attachment should ask for confirmation', async function (asse
res_id: 100,
res_model: 'res.partner',
});
const { createChatterContainerComponent } = await this.start();
const { createChatterContainerComponent } = await this.start({ hasDialog: true });
await createChatterContainerComponent({
isAttachmentBoxVisibleInitially: true,
threadId: 100,
@@ -29,13 +29,7 @@ export class AttachmentCard extends Component {
}
Object.assign(AttachmentCard, {
props: {
localId: String,
onAttachmentRemoved: {
type: Function,
optional: true,
},
},
props: { localId: String },
template: 'mail.AttachmentCard',
});
@@ -56,13 +56,6 @@
</t>
</div>
</t>
<t t-if="attachmentCard.hasDeleteConfirmDialog">
<AttachmentDeleteConfirm
attachmentLocalId="attachmentCard.attachment.localId"
onAttachmentRemoved="props.onAttachmentRemoved"
onClosed="attachmentCard.onDeleteConfirmDialogClosed"
/>
</t>
</div>
</t>
</div>
@@ -1,80 +1,31 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model/use_component_to_model';
import Dialog from 'web.OwlDialog';
const { Component, escape } = owl;
const { Component } = owl;
export class AttachmentDeleteConfirm extends Component {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {Attachment}
* @override
*/
get attachment() {
return this.messaging && this.messaging.models['Attachment'].get(this.props.attachmentLocalId);
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'AttachmentDeleteConfirmView' });
}
/**
* @returns {string}
* @returns {AttachmentDeleteConfirmView}
*/
getBody() {
return _.str.sprintf(
this.env._t(`Do you really want to delete "%s"?`),
escape(this.attachment.displayName)
);
}
/**
* @returns {string}
*/
getTitle() {
return this.env._t("Confirmation");
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onClickCancel() {
this.root.comp._close();
}
/**
* @private
*/
async _onClickOk() {
await this.attachment.remove();
this.root.comp._close();
if (this.props.onAttachmentRemoved) {
this.props.onAttachmentRemoved({
attachmentLocalId: this.props.attachmentLocalId,
});
}
get attachmentDeleteConfirmView() {
return this.messaging && this.messaging.models['AttachmentDeleteConfirmView'].get(this.props.localId);
}
}
Object.assign(AttachmentDeleteConfirm, {
components: { Dialog },
props: {
attachmentLocalId: String,
onAttachmentRemoved: {
type: Function,
optional: true,
},
onClosed: {
type: Function,
optional: true,
}
},
props: { localId: String },
template: 'mail.AttachmentDeleteConfirm',
});
@@ -1,12 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.AttachmentDeleteConfirm" owl="1">
<Dialog contentClass="'o_AttachmentDeleteConfirm'" title="getTitle()" size="'medium'" onClosed="props.onClosed" t-ref="root">
<p class="o_AttachmentDeleteConfirm_mainText" t-esc="getBody()"/>
<t t-set-slot="buttons">
<button class="o_AttachmentDeleteConfirm_confirmButton btn btn-primary" t-on-click="_onClickOk">Ok</button>
<button class="o_AttachmentDeleteConfirm_cancelButton btn btn-secondary" t-on-click="_onClickCancel">Cancel</button>
<div class="o_AttachmentDeleteConfirm card bg-white" t-attf-class="{{ className }}" t-ref="root">
<t t-if="attachmentDeleteConfirmView">
<h4 class="m-3">Confirmation</h4>
<hr class="mt-0 mb-3"/>
<p class="o_AttachmentDeleteConfirm_mainText mx-3 mb-3" t-esc="attachmentDeleteConfirmView.body"/>
<hr class="mt-0 mb-3"/>
<div class="o_AttachmentDeleteConfirm_buttons mx-3 mb-3">
<button class="o_AttachmentDeleteConfirm_confirmButton btn btn-primary me-2" t-on-click="attachmentDeleteConfirmView.onClickOk">Ok</button>
<button class="o_AttachmentDeleteConfirm_cancelButton btn btn-secondary me-2" t-on-click="attachmentDeleteConfirmView.onClickCancel">Cancel</button>
</div>
</t>
</Dialog>
</div>
</t>
</templates>
@@ -25,13 +25,7 @@ export class AttachmentImage extends Component {
}
Object.assign(AttachmentImage, {
props: {
localId: String,
onAttachmentRemoved: {
type: Function,
optional: true,
},
},
props: { localId: String },
template: 'mail.AttachmentImage',
});
@@ -29,13 +29,6 @@
</div>
</div>
</t>
<t t-if="attachmentImage.hasDeleteConfirmDialog">
<AttachmentDeleteConfirm
attachmentLocalId="attachmentImage.attachment.localId"
onAttachmentRemoved="props.onAttachmentRemoved"
onClosed="attachmentImage.onDeleteConfirmDialogClosed"
/>
</t>
</div>
</t>
</div>
@@ -16,13 +16,7 @@ export class AttachmentList extends Component {
}
Object.assign(AttachmentList, {
props: {
localId: String,
onAttachmentRemoved: {
type: Function,
optional: true,
},
},
props: { localId: String },
template: 'mail.AttachmentList',
});
@@ -6,12 +6,12 @@
<t t-if="attachmentList">
<div class="o_AttachmentList_partialList o_AttachmentList_partialListImages d-flex flex-grow-1 flex-wrap">
<t t-foreach="attachmentList.attachmentImages" t-as="attachmentImage" t-key="attachmentImage.localId">
<AttachmentImage className="'o_AttachmentList_attachment my-1 me-1 mw-100'" localId="attachmentImage.localId" onAttachmentRemoved="props.onAttachmentRemoved"/>
<AttachmentImage className="'o_AttachmentList_attachment my-1 me-1 mw-100'" localId="attachmentImage.localId"/>
</t>
</div>
<div class="o_AttachmentList_partialList o_AttachmentList_partialListNonImages d-flex flex-grow-1 flex-wrap justify-content-start m-1">
<t t-foreach="attachmentList.attachmentCards" t-as="attachmentCard" t-key="attachmentCard.localId">
<AttachmentCard className="'o_AttachmentList_attachment my-1 me-1 mw-100'" localId="attachmentCard.localId" onAttachmentRemoved="props.onAttachmentRemoved"/>
<AttachmentCard className="'o_AttachmentList_attachment my-1 me-1 mw-100'" localId="attachmentCard.localId"/>
</t>
</div>
</t>
@@ -272,6 +272,7 @@ QUnit.test('clicking on the delete attachment button multiple times should do th
assert.expect(2);
const { createMessageComponent } = await this.start({
hasDialog: true,
async mockRPC(route, args) {
if (route === '/mail/attachment/delete') {
assert.step('attachment_unlink');
@@ -1,9 +1,8 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import Dialog from 'web.OwlDialog';
const { Component } = owl;
export class DeleteMessageConfirm extends Component {
@@ -13,24 +12,20 @@ export class DeleteMessageConfirm extends Component {
*/
setup() {
super.setup();
this.title = this.env._t("Confirmation");
useComponentToModel({ fieldName: 'component', modelName: 'DeleteMessageConfirmView' });
}
/**
* @returns {MessageActionList}
* @returns {DeleteMessageConfirmView}
*/
get messageActionList() {
return this.messaging && this.messaging.models['MessageActionList'].get(this.props.localId);
get deleteMessageConfirmView() {
return this.messaging && this.messaging.models['DeleteMessageConfirmView'].get(this.props.localId);
}
}
Object.assign(DeleteMessageConfirm, {
components: {
Dialog,
},
props: {
localId: String,
},
props: { localId: String },
template: 'mail.DeleteMessageConfirm',
});
@@ -1,18 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.DeleteMessageConfirm" owl="1">
<Dialog contentClass="'o_DeleteMessageConfirm ' + className" title="title" onClosed="messageActionList and messageActionList.onDeleteConfirmDialogClosed" t-ref="root">
<t t-if="messageActionList">
<p>Are you sure you want to delete this message?</p>
<blockquote class="o_DeleteMessageConfirm_blockquote">
<Message localId="messageActionList.messageViewForDelete.localId"/>
<div class="o_DeleteMessageConfirm card bg-white" t-attf-class="{{ className }}" t-ref="root">
<t t-if="deleteMessageConfirmView">
<h4 class="m-3">Confirmation</h4>
<hr class="mt-0 mb-3"/>
<p class="mx-3 mb-3">Are you sure you want to delete this message?</p>
<blockquote class="o_DeleteMessageConfirm_blockquote mx-3 mb-3">
<Message localId="deleteMessageConfirmView.messageView.localId"/>
</blockquote>
<small t-if="!messageActionList.message.originThread or messageActionList.message.originThread.model !== 'mail.channel'">Pay attention: The followers of this document who were notified by email will still be able to read the content of this message and reply to it.</small>
<t t-set-slot="buttons">
<button class="btn btn-primary" t-on-click="messageActionList.onClickConfirmDelete">Delete</button>
<button class="btn btn-secondary" t-on-click="root.comp._close()">Cancel</button>
</t>
<small t-if="!deleteMessageConfirmView.message.originThread or deleteMessageConfirmView.message.originThread.model !== 'mail.channel'" class="mx-3 mb-3">Pay attention: The followers of this document who were notified by email will still be able to read the content of this message and reply to it.</small>
<hr class="mt-0 mb-3"/>
<div class="o_DeleteMessageConfirm_buttons mx-3 mb-3">
<button class="btn btn-primary me-2" t-on-click="deleteMessageConfirmView.onClickDelete">Delete</button>
<button class="btn btn-secondary me-2" t-on-click="deleteMessageConfirmView.onClickCancel">Cancel</button>
</div>
</t>
</Dialog>
</div>
</t>
</templates>
@@ -14,10 +14,12 @@
z-index: $zindex-modal;
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_Dialog {
background-color: rgba($black, 0.7);
.o_Dialog_componentLargeSize {
min-width: Min(95vw, $o-modal-lg);
max-width: 95vw;
}
.o_Dialog_componentMediumSize {
min-width: Min(95vw, $o-modal-md);
max-width: 95vw;
}
@@ -2,12 +2,14 @@
<templates xml:space="preserve">
<t t-name="mail.Dialog" owl="1">
<div class="o_Dialog" t-attf-class="{{ className }}" t-ref="root">
<div class="o_Dialog" t-attf-class="{{ className }}" t-att-style="dialog and dialog.style" t-ref="root">
<t t-if="dialog">
<t
t-component="constructor.components[dialog.componentName]"
className="'o_Dialog_component'"
t-props="{ localId: dialog.record.localId }"
t-props="{
className: `o_Dialog_component ${dialog.componentClassName}`,
localId: dialog.record.localId,
}"
/>
</t>
</div>
@@ -153,7 +153,7 @@ export class Message extends Component {
this.messageView.messageActionList &&
(
this.messageView.messageActionList.reactionPopoverView ||
this.messageView.messageActionList.showDeleteConfirm
this.messageView.messageActionList.deleteConfirmDialog
)
)
);
@@ -543,15 +543,6 @@ export class Message extends Component {
this.messageView.message.author.openProfile();
}
/**
* @private
* @param {MouseEvent} ev
*/
_onClickFailure(ev) {
markEventHandled(ev, 'Message.ClickFailure');
this.messageView.message.openResendAction();
}
/**
* @private
* @param {MouseEvent} ev
@@ -102,7 +102,7 @@
</t>
<t t-if="threadView and messageView.message.originThread and messageView.message.originThread === threadView.thread and messageView.message.notifications.length > 0">
<t t-if="messageView.message.failureNotifications.length > 0">
<span class="o_Message_notificationIconClickable o-error" t-on-click="_onClickFailure">
<span class="o_Message_notificationIconClickable o-error" t-on-click="messageView.onClickFailure">
<i name="failureIcon" class="o_Message_notificationIcon fa fa-envelope"/>
</span>
</t>
@@ -654,7 +654,7 @@ QUnit.test('only show messaging seen indicator if authored by me, after last see
QUnit.test('allow attachment delete on authored message', async function (assert) {
assert.expect(5);
const { createMessageComponent } = await this.start();
const { createMessageComponent } = await this.start({ hasDialog: true });
const message = this.messaging.models['Message'].create({
attachments: insertAndReplace({
filename: "BLAH.jpg",
@@ -14,7 +14,6 @@
<span t-if="messageActionList.hasMarkAsReadIcon" class="o_MessageActionList_action o_MessageActionList_actionMarkRead p-2 fa fa-lg fa-check" title="Mark as Read" t-on-click="messageActionList.onClickMarkAsRead"/>
<span t-if="messageActionList.message.canBeDeleted" class="o_MessageActionList_action o_MessageActionList_actionEdit p-2 fa fa-lg fa-pencil" title="Edit" t-on-click="messageActionList.onClickEdit"/>
<span t-if="messageActionList.message.canBeDeleted" class="o_MessageActionList_action o_MessageActionList_actionDelete p-2 fa fa-lg fa-trash" title="Delete" t-on-click="messageActionList.onClickDelete"/>
<DeleteMessageConfirm t-if="messageActionList.showDeleteConfirm" localId="messageActionList.localId"/>
</t>
</div>
</t>
@@ -986,7 +986,7 @@ QUnit.test("delete all attachments of message without content should no longer d
res_id: 11,
}
);
const { createThreadViewComponent } = await this.start();
const { createThreadViewComponent } = await this.start({ hasDialog: true });
const threadViewer = this.messaging.models['ThreadViewer'].create({
hasThreadView: true,
qunitTest: insertAndReplace(),
@@ -1046,7 +1046,7 @@ QUnit.test('delete all attachments of a message with some text content should st
res_id: 11,
},
);
const { createThreadViewComponent } = await this.start();
const { createThreadViewComponent } = await this.start({ hasDialog: true });
const threadViewer = this.messaging.models['ThreadViewer'].create({
hasThreadView: true,
qunitTest: insertAndReplace(),
@@ -1113,7 +1113,7 @@ QUnit.test('delete all attachments of a message with tracking fields should stil
new_value: "New name",
old_value: "Old name",
});
const { createThreadViewComponent } = await this.start();
const { createThreadViewComponent } = await this.start({ hasDialog: true });
const threadViewer = this.messaging.models['ThreadViewer'].create({
hasThreadView: true,
qunitTest: insertAndReplace(),
@@ -8,13 +8,6 @@ registerModel({
name: 'AttachmentBoxView',
identifyingFields: ['chatter'],
recordMethods: {
/**
* Handles attachment removed event.
*/
onAttachmentRemoved() {
// FIXME Could be changed by spying attachments count (task-2252858)
this.component.trigger('o-attachments-changed');
},
/**
* Handles click on the "add attachment" button.
*/
@@ -34,18 +34,9 @@ registerModel({
this.component.trigger('o-attachment-removed', { attachmentLocalId: this.attachment.localId });
this.attachment.remove();
} else {
this.update({ hasDeleteConfirmDialog: true });
this.update({ attachmentDeleteConfirmDialog: insertAndReplace() });
}
},
/**
* Synchronizes the `hasDeleteConfirmDialog` when the dialog is closed.
*/
onDeleteConfirmDialogClosed() {
if (!this.exists()) {
return;
}
this.update({ hasDeleteConfirmDialog: false });
},
},
fields: {
/**
@@ -55,6 +46,10 @@ registerModel({
readonly: true,
required: true,
}),
attachmentDeleteConfirmDialog: one('Dialog', {
inverse: 'attachmentCardOwnerAsAttachmentDeleteConfirm',
isCausal: true,
}),
/**
* States the attachmentList displaying this card.
*/
@@ -67,11 +62,5 @@ registerModel({
* States the OWL component of this attachment card.
*/
component: attr(),
/**
* Determines the status of the delete confirm dialog (open/closed).
*/
hasDeleteConfirmDialog: attr({
default: false,
}),
},
});
@@ -0,0 +1,90 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear, replace } from '@mail/model/model_field_command';
registerModel({
name: 'AttachmentDeleteConfirmView',
identifyingFields: ['dialogOwner'],
recordMethods: {
/**
* Returns whether the given html element is inside this attachment delete confirm view.
*
* @param {Element} element
* @returns {boolean}
*/
containsElement(element) {
return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element));
},
onClickCancel() {
this.dialogOwner.delete();
},
async onClickOk() {
await this.attachment.remove();
if (this.chatter && this.chatter.component) {
this.chatter.component.trigger('o-attachments-changed');
}
},
/**
* @private
* @returns {FieldCommand}
*/
_computeAttachment() {
if (this.dialogOwner && this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm) {
return replace(this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachment);
}
if (this.dialogOwner && this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm) {
return replace(this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachment);
}
return clear();
},
/**
* @private
* @returns {string}
*/
_computeBody() {
return _.str.sprintf(this.env._t(`Do you really want to delete "%s"?`), this.attachment.displayName);
},
/**
* @private
* @returns {FieldCommand}
*/
_computeChatter() {
if (
this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm &&
this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachmentList.attachmentBoxViewOwner &&
this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachmentList.attachmentBoxViewOwner.chatter
) {
return replace(this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachmentList.attachmentBoxViewOwner.chatter);
}
if (
this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm &&
this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachmentList.attachmentBoxViewOwner &&
this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachmentList.attachmentBoxViewOwner.chatter
) {
return replace(this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachmentList.attachmentBoxViewOwner.chatter);
}
return clear();
},
},
fields: {
attachment: one('Attachment', {
compute: '_computeAttachment',
readonly: true,
required: true,
}),
body: attr({
compute: '_computeBody',
}),
chatter: one('Chatter', {
compute: '_computeChatter',
}),
component: attr(),
dialogOwner: one('Dialog', {
inverse: 'attachmentDeleteConfirmView',
readonly: true,
required: true,
}),
},
});
@@ -34,18 +34,9 @@ registerModel({
this.component.trigger('o-attachment-removed', { attachmentLocalId: this.attachment.localId });
this.attachment.remove();
} else {
this.update({ hasDeleteConfirmDialog: true });
this.update({ attachmentDeleteConfirmDialog: insertAndReplace() });
}
},
/**
* Synchronize the `hasDeleteConfirmDialog` when the dialog is closed.
*/
onDeleteConfirmDialogClosed() {
if (!this.exists()) {
return;
}
this.update({ hasDeleteConfirmDialog: false });
},
/**
* @private
* @returns {number}
@@ -97,6 +88,10 @@ registerModel({
readonly: true,
required: true,
}),
attachmentDeleteConfirmDialog: one('Dialog', {
inverse: 'attachmentImageOwnerAsAttachmentDeleteConfirm',
isCausal: true,
}),
/**
* States the attachmentList displaying this attachment image.
*/
@@ -109,12 +104,6 @@ registerModel({
* States the OWL component of this attachment image.
*/
component: attr(),
/**
* Determines the status of the delete confirm dialog (open/closed).
*/
hasDeleteConfirmDialog: attr({
default: false,
}),
/**
* Determines the max height of this attachment image in px.
*/
@@ -459,7 +459,7 @@ registerModel({
async updateMessage() {
const composer = this.composer;
if (!composer.textInputContent) {
this.messageViewInEditing.messageActionList.update({ showDeleteConfirm: true });
this.messageViewInEditing.messageActionList.update({ deleteConfirmDialog: insertAndReplace() });
return;
}
const escapedAndCompactContent = escapeAndCompactTextContent(composer.textInputContent);
@@ -0,0 +1,68 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear, insertAndReplace, replace } from '@mail/model/model_field_command';
registerModel({
name: 'DeleteMessageConfirmView',
identifyingFields: ['dialogOwner'],
recordMethods: {
/**
* Returns whether the given html element is inside this delete message confirm view.
*
* @param {Element} element
* @returns {boolean}
*/
containsElement(element) {
return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element));
},
onClickCancel() {
this.dialogOwner.delete();
},
onClickDelete() {
this.message.updateContent({
attachment_ids: [],
body: '',
});
},
/**
* @private
* @returns {FieldCommand}
*/
_computeMessage() {
return replace(this.dialogOwner.messageActionListOwnerAsDeleteConfirm.message);
},
/**
* @private
* @returns {MessageView}
*/
_computeMessageView() {
return this.message ? insertAndReplace({ message: replace(this.message) }) : clear();
},
},
fields: {
component: attr(),
dialogOwner: one('Dialog', {
inverse: 'deleteMessageConfirmView',
readonly: true,
required: true,
}),
message: one('Message', {
compute: '_computeMessage',
readonly: true,
required: true,
}),
/**
* Determines the message view that this delete message confirm view
* will use to display this message.
*/
messageView: one('MessageView', {
compute: '_computeMessageView',
inverse: 'deleteMessageConfirmViewOwner',
isCausal: true,
readonly: true,
required: true,
}),
},
});
+97 -4
View File
@@ -7,8 +7,11 @@ import { clear, insertAndReplace, replace } from '@mail/model/model_field_comman
registerModel({
name: 'Dialog',
identifyingFields: [[
'attachmentCardOwnerAsAttachmentDeleteConfirm',
'attachmentImageOwnerAsAttachmentDeleteConfirm',
'attachmentListOwnerAsAttachmentView',
'followerOwnerAsSubtypeList',
'messageActionListOwnerAsDeleteConfirm',
]],
recordMethods: {
/**
@@ -18,6 +21,19 @@ registerModel({
hasElementInContent(element) {
return Boolean(this.record && this.record.containsElement(element));
},
/**
* @private
* @returns {FieldCommand}
*/
_computeAttachmentDeleteConfirmView() {
if (this.attachmentCardOwnerAsAttachmentDeleteConfirm) {
return insertAndReplace();
}
if (this.attachmentImageOwnerAsAttachmentDeleteConfirm) {
return insertAndReplace();
}
return clear();
},
/**
* @private
* @returns {FieldCommand}
@@ -28,6 +44,29 @@ registerModel({
}
return clear();
},
/**
* @private
* @returns {number}
*/
_computeBackgroundOpacity() {
if (this.attachmentViewer) {
return 0.7;
}
return 0.5;
},
/**
* @private
* @returns {string}
*/
_computeComponentClassName() {
if (this.attachmentDeleteConfirmView) {
return 'o_Dialog_componentMediumSize align-self-start mt-5';
}
if (this.deleteMessageConfirmView) {
return 'o_Dialog_componentLargeSize align-self-start mt-5';
}
return '';
},
/**
* @private
* @returns {string|FieldCommand}
@@ -36,20 +75,30 @@ registerModel({
if (this.attachmentViewer) {
return 'AttachmentViewer';
}
if (this.attachmentDeleteConfirmView) {
return 'AttachmentDeleteConfirm';
}
if (this.deleteMessageConfirmView) {
return 'DeleteMessageConfirm';
}
if (this.followerSubtypeList) {
return 'FollowerSubtypeList';
}
return clear();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeDeleteMessageConfirmView() {
return this.messageActionListOwnerAsDeleteConfirm ? insertAndReplace() : clear();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeFollowerSubtypeList() {
if (this.followerOwnerAsSubtypeList) {
return insertAndReplace();
}
return clear();
return this.followerOwnerAsSubtypeList ? insertAndReplace() : clear();
},
/**
/**
@@ -84,12 +133,38 @@ registerModel({
if (this.attachmentViewer) {
return replace(this.attachmentViewer);
}
if (this.attachmentDeleteConfirmView) {
return replace(this.attachmentDeleteConfirmView);
}
if (this.deleteMessageConfirmView) {
return replace(this.deleteMessageConfirmView);
}
if (this.followerSubtypeList) {
return replace(this.followerSubtypeList);
}
},
/**
* @private
* @returns {string}
*/
_computeStyle() {
return `background-color: rgba(0, 0, 0, ${this.backgroundOpacity});`;
},
},
fields: {
attachmentCardOwnerAsAttachmentDeleteConfirm: one('AttachmentCard', {
inverse: 'attachmentDeleteConfirmDialog',
readonly: true,
}),
attachmentDeleteConfirmView: one('AttachmentDeleteConfirmView', {
compute: '_computeAttachmentDeleteConfirmView',
inverse: 'dialogOwner',
isCausal: true,
}),
attachmentImageOwnerAsAttachmentDeleteConfirm: one('AttachmentImage', {
inverse: 'attachmentDeleteConfirmDialog',
readonly: true,
}),
attachmentListOwnerAsAttachmentView: one('AttachmentList', {
inverse: 'attachmentListViewDialog',
readonly: true,
@@ -99,10 +174,21 @@ registerModel({
inverse: 'dialogOwner',
isCausal: true,
}),
backgroundOpacity: attr({
compute: '_computeBackgroundOpacity',
}),
componentClassName: attr({
compute: '_computeComponentClassName',
}),
componentName: attr({
compute: '_computeComponentName',
required: true,
}),
deleteMessageConfirmView: one('DeleteMessageConfirmView', {
compute: '_computeDeleteMessageConfirmView',
inverse: 'dialogOwner',
isCausal: true,
}),
followerOwnerAsSubtypeList: one('Follower', {
inverse: 'followerSubtypeListDialog',
readonly: true,
@@ -121,6 +207,10 @@ registerModel({
inverse: 'dialogs',
readonly: true,
}),
messageActionListOwnerAsDeleteConfirm: one('MessageActionList', {
inverse: 'deleteConfirmDialog',
readonly: true,
}),
/**
* Content of dialog that is directly linked to a record that models
* a UI component, such as AttachmentViewer. These records must be
@@ -132,5 +222,8 @@ registerModel({
readonly: true,
required: true,
}),
style: attr({
compute: '_computeStyle',
}),
},
});
@@ -16,22 +16,12 @@ registerModel({
onClick(ev) {
markEventHandled(ev, 'MessageActionList.Click');
},
/**
* @private
* @param {MouseEvent} ev
*/
onClickConfirmDelete(ev) {
this.message.updateContent({
body: '',
attachment_ids: [],
});
},
/**
* @private
* @param {MouseEvent} ev
*/
onClickDelete(ev) {
this.update({ showDeleteConfirm: true });
this.update({ deleteConfirmDialog: insertAndReplace() });
},
/**
* @private
@@ -82,13 +72,6 @@ registerModel({
onClickToggleStar(ev) {
this.message.toggleStar();
},
/**
* @private
* @param {CustomEvent} ev
*/
onDeleteConfirmDialogClosed(ev) {
this.update({ showDeleteConfirm: false });
},
/**
* @private
* @returns {boolean}
@@ -114,21 +97,16 @@ registerModel({
)
);
},
/**
* @private
* @returns {MessageView}
*/
_computeMessageViewForDelete() {
return this.message
? insertAndReplace({ message: replace(this.message) })
: clear();
},
},
fields: {
/**
* States the reference to the reaction action in the component.
*/
actionReactionRef: attr(),
deleteConfirmDialog: one('Dialog', {
inverse: 'messageActionListOwnerAsDeleteConfirm',
isCausal: true,
}),
/**
* Determines whether this message action list has mark as read icon.
*/
@@ -155,15 +133,6 @@ registerModel({
readonly: true,
required: true,
}),
/**
* Determines the message view that this message action list will use to
* display this message in this delete confirmation dialog.
*/
messageViewForDelete: one('MessageView', {
compute: '_computeMessageViewForDelete',
inverse: 'messageActionListWithDelete',
isCausal: true,
}),
/**
* Determines the reaction popover that is active on this message action list.
*/
@@ -171,11 +140,5 @@ registerModel({
inverse: 'messageActionListOwnerAsReaction',
isCausal: true,
}),
/**
* Determines whether to show the message delete-confirm dialog.
*/
showDeleteConfirm: attr({
default: false,
}),
},
});
@@ -3,10 +3,11 @@
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear, insertAndReplace, replace } from '@mail/model/model_field_command';
import { markEventHandled } from '@mail/utils/utils';
registerModel({
name: 'MessageView',
identifyingFields: [['threadView', 'messageActionListWithDelete'], 'message'],
identifyingFields: [['threadView', 'deleteMessageConfirmViewOwner'], 'message'],
recordMethods: {
/**
* Briefly highlights the message.
@@ -20,6 +21,13 @@ registerModel({
}, 2000),
});
},
/**
* @param {MouseEvent} ev
*/
onClickFailure(ev) {
markEventHandled(ev, 'Message.ClickFailure');
this.message.openResendAction();
},
onComponentUpdate() {
if (!this.exists()) {
return;
@@ -110,9 +118,7 @@ registerModel({
* @returns {FieldCommand}
*/
_computeMessageActionList() {
return (!this.messageActionListWithDelete)
? insertAndReplace()
: clear();
return this.deleteMessageConfirmViewOwner ? clear() : insertAndReplace();
},
/**
* @private
@@ -153,6 +159,14 @@ registerModel({
inverse: 'messageViewInEditing',
isCausal: true,
}),
/**
* States the delete message confirm view that is displaying this
* message view.
*/
deleteMessageConfirmViewOwner: one('DeleteMessageConfirmView', {
inverse: 'messageView',
readonly: true,
}),
/**
* Determines whether this message view should be highlighted at next
* render. Scrolls into view and briefly highlights it.
@@ -189,15 +203,6 @@ registerModel({
isCausal: true,
readonly: true,
}),
/**
* States the message action list that is displaying this message view
* in its delete confirmation view.
*/
messageActionListWithDelete: one('MessageActionList', {
inverse: 'messageViewForDelete',
isCausal: true,
readonly: true,
}),
/**
* Determines the message that is displayed by this message view.
*/
@@ -1,76 +0,0 @@
/** @odoo-module **/
import { Message } from '@mail/components/message/message';
import { patch } from 'web.utils';
const { useState } = owl;
patch(Message.prototype, 'snailmail/static/src/components/message/message.js', {
/**
* @override
*/
setup() {
this._super();
this._onDialogClosedSnailmailError = this._onDialogClosedSnailmailError.bind(this);
this.snailmailState = useState({
// Determine if the error dialog is displayed.
hasDialog: false,
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
*/
_onClickFailure() {
if (this.messageView.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.messageView.message.notifications[0].failure_type) {
case 'sn_credit':
// URL only used in this component, not received at init
this.messaging.fetchSnailmailCreditsUrl();
this.snailmailState.hasDialog = true;
break;
case 'sn_error':
this.snailmailState.hasDialog = true;
break;
case 'sn_fields':
this.messageView.message.openMissingFieldsLetterAction();
break;
case 'sn_format':
this.messageView.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.messaging.fetchSnailmailCreditsUrlTrial();
this.snailmailState.hasDialog = true;
break;
}
} else {
this._super(...arguments);
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onDialogClosedSnailmailError() {
this.snailmailState.hasDialog = false;
},
});
@@ -16,12 +16,6 @@
<t t-else="">$0</t>
</xpath>
<xpath expr="//*[@name='rootCondition']" position="inside">
<t t-if="snailmailState.hasDialog">
<SnailmailError messageLocalId="messageView.message.localId" onClosed="_onDialogClosedSnailmailError"/>
</t>
</xpath>
<!--
It was decided that the information displayed for snailmail messages
has to be different than for standard messages, see task-1907998.
@@ -281,6 +281,7 @@ QUnit.test('No Price Available', async function (assert) {
res_partner_id: 12,
});
const { createThreadViewComponent } = await this.start({
hasDialog: true,
async mockRPC(route, args) {
if (args.method === 'cancel_letter' && args.model === 'mail.message' && args.args[0][0] === 10) {
assert.step(args.method);
@@ -373,6 +374,7 @@ QUnit.test('Credit Error', async function (assert) {
res_partner_id: 12,
});
const { createThreadViewComponent } = await this.start({
hasDialog: true,
async mockRPC(route, args) {
if (args.method === 'send_letter' && args.model === 'mail.message' && args.args[0][0] === 10) {
assert.step(args.method);
@@ -470,6 +472,7 @@ QUnit.test('Trial Error', async function (assert) {
res_partner_id: 12,
});
const { createThreadViewComponent } = await this.start({
hasDialog: true,
async mockRPC(route, args) {
if (args.method === 'send_letter' && args.model === 'mail.message' && args.args[0][0] === 10) {
assert.step(args.method);
@@ -1,90 +1,32 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import Dialog from 'web.OwlDialog';
const { Component } = owl;
class SnailmailError extends Component {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
export class SnailmailError extends Component {
/**
* @returns {boolean}
* @override
*/
get hasCreditsError() {
return (
this.notification.failure_type === 'sn_credit' ||
this.notification.failure_type === 'sn_trial'
);
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'SnailmailErrorView' });
}
/**
* @returns {Message}
* @returns {SnailmailErrorView}
*/
get message() {
return this.messaging && this.messaging.models['Message'].get(this.props.messageLocalId);
}
/**
* @returns {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.root.comp._close();
this.message.cancelLetter();
}
/**
* @private
*/
_onClickClose() {
this.root.comp._close();
}
/**
* @private
*/
_onClickResendLetter() {
this.root.comp._close();
this.message.resendLetter();
get snailmailErrorView() {
return this.messaging && this.messaging.models['SnailmailErrorView'].get(this.props.localId);
}
}
Object.assign(SnailmailError, {
components: { Dialog },
props: {
messageLocalId: String,
onClosed: {
type: Function,
optional: true,
},
},
props: { localId: String },
template: 'snailmail.SnailmailError',
});
registerMessagingComponent(SnailmailError);
export default SnailmailError;
@@ -2,60 +2,62 @@
<templates xml:space="preserve">
<t t-name="snailmail.SnailmailError" owl="1">
<Dialog contentClass="'o_SnailmailError'" title="title" size="'medium'" onClosed="props.onClosed" t-ref="root">
<t t-if="message and notification">
<t t-if="notification.failure_type === 'sn_credit'">
<p class="o_SnailmailError_contentCredit">
<div class="o_SnailmailError card bg-white" t-attf-class="{{ className }}" t-ref="root">
<t t-if="snailmailErrorView">
<h4 class="m-3">Failed letter</h4>
<hr class="mt-0 mb-3"/>
<t t-if="snailmailErrorView.notification.failure_type === 'sn_credit'">
<p class="o_SnailmailError_contentCredit mx-3 mb-3">
The letter could not be sent due to insufficient credits on your IAP account.
</p>
<t t-if="messaging.snailmail_credits_url">
<div class="text-right">
<div class="text-right mx-3 mb-3">
<a class="btn btn-link" t-att-href="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_SnailmailError_contentTrial">
<t t-elif="snailmailErrorView.notification.failure_type === 'sn_trial'">
<p class="o_SnailmailError_contentTrial mx-3 mb-3">
You need credits on your IAP account to send a letter.
</p>
<t t-if="messaging.snailmail_credits_url_trial">
<div class="text-right">
<div class="text-right mx-3 mb-3">
<a class="btn btn-link" t-att-href="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_SnailmailError_contentPrice">
<t t-elif="snailmailErrorView.notification.failure_type === 'sn_price'">
<p class="o_SnailmailError_contentPrice mx-3 mb-3">
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_SnailmailError_contentError">
<t t-elif="snailmailErrorView.notification.failure_type === 'sn_error'">
<p class="o_SnailmailError_contentError mx-3 mb-3">
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_SnailmailError_resendLetterButton btn btn-primary" t-on-click="_onClickResendLetter">Re-send letter</button>
<hr class="mt-0 mb-3"/>
<div class="o_SnailmailError_buttons mx-3 mb-3">
<t t-if="snailmailErrorView.hasCreditsError">
<button class="o_SnailmailError_resendLetterButton btn btn-primary me-2" t-on-click="snailmailErrorView.onClickResendLetter">Re-send letter</button>
</t>
<button class="o_SnailmailError_cancelLetterButton btn"
<button class="o_SnailmailError_cancelLetterButton btn me-2"
t-att-class="{
'btn-primary': !hasCreditsError,
'btn-secondary': hasCreditsError,
'btn-primary': !snailmailErrorView.hasCreditsError,
'btn-secondary': snailmailErrorView.hasCreditsError,
}"
t-on-click="_onClickCancelLetter"
t-on-click="snailmailErrorView.onClickCancelLetter"
>
Cancel letter
</button>
<button class="o_SnailmailError_closeButton btn btn-secondary" t-on-click="_onClickClose">Close</button>
</t>
<button class="o_SnailmailError_closeButton btn btn-secondary me-2" t-on-click="snailmailErrorView.onClickClose">Close</button>
</div>
</t>
</Dialog>
</div>
</t>
</templates>
@@ -0,0 +1,67 @@
/** @odoo-module **/
import { addFields, addRecordMethods, patchIdentifyingFields, patchRecordMethods } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
import { clear, insertAndReplace, replace } from '@mail/model/model_field_command';
// ensure that the model definition is loaded before the patch
import '@mail/models/dialog/dialog';
patchIdentifyingFields('Dialog', identifyingFields => {
identifyingFields[0].push('messageViewOwnerAsSnailmailError');
});
addFields('Dialog', {
messageViewOwnerAsSnailmailError: one('MessageView', {
inverse: 'snailmailErrorDialog',
readonly: true,
}),
snailmailErrorView: one('SnailmailErrorView', {
compute: '_computeSnailmailErrorView',
inverse: 'dialogOwner',
isCausal: true,
}),
});
addRecordMethods('Dialog', {
/**
* @private
* @returns {FieldCommand}
*/
_computeSnailmailErrorView() {
if (this.messageViewOwnerAsSnailmailError) {
return insertAndReplace();
}
return clear();
},
});
patchRecordMethods('Dialog', {
/**
* @private
* @returns {string}
*/
_computeComponentClassName() {
if (this.snailmailErrorView) {
return 'o_Dialog_componentMediumSize align-self-start mt-5';
}
return this._super();
},
/**
* @override
*/
_computeComponentName() {
if (this.snailmailErrorView) {
return 'SnailmailError';
}
return this._super();
},
/**
* @override
*/
_computeRecord() {
if (this.snailmailErrorView) {
return replace(this.snailmailErrorView);
}
return this._super();
},
});
@@ -0,0 +1,56 @@
/** @odoo-module **/
import { addFields, patchRecordMethods } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
import { insertAndReplace } from '@mail/model/model_field_command';
// ensure that the model definition is loaded before the patch
import '@mail/models/message_view/message_view';
addFields('MessageView', {
snailmailErrorDialog: one('Dialog', {
inverse: 'messageViewOwnerAsSnailmailError',
isCausal: true,
}),
});
patchRecordMethods('MessageView', {
/**
* @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.messaging.fetchSnailmailCreditsUrl();
this.update({ snailmailErrorDialog: insertAndReplace() });
break;
case 'sn_error':
this.update({ snailmailErrorDialog: insertAndReplace() });
break;
case 'sn_fields':
this.message.openMissingFieldsLetterAction();
break;
case 'sn_format':
this.message.openFormatLetterAction();
break;
case 'sn_price':
this.update({ snailmailErrorDialog: insertAndReplace() });
break;
case 'sn_trial':
// URL only used in this component, not received at init
this.messaging.fetchSnailmailCreditsUrlTrial();
this.update({ snailmailErrorDialog: insertAndReplace() });
break;
}
} else {
this._super(...arguments);
}
},
});
@@ -0,0 +1,81 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { replace } from '@mail/model/model_field_command';
registerModel({
name: 'SnailmailErrorView',
identifyingFields: ['dialogOwner'],
recordMethods: {
/**
* Returns whether the given html element is inside this snailmail error view.
*
* @param {Element} element
* @returns {boolean}
*/
containsElement(element) {
return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element));
},
onClickCancelLetter() {
this.message.cancelLetter();
this.dialogOwner.delete();
},
onClickClose() {
this.dialogOwner.delete();
},
onClickResendLetter() {
this.message.resendLetter();
this.dialogOwner.delete();
},
/**
* @private
* @returns {boolean}
*/
_computeHasCreditsError() {
return Boolean(
this.notification &&
(
this.notification.failure_type === 'sn_credit' ||
this.notification.failure_type === 'sn_trial'
)
);
},
/**
* @private
* @returns {FieldCommand}
*/
_computeMessage() {
return replace(this.dialogOwner.messageViewOwnerAsSnailmailError.message);
},
/**
* @private
* @returns {FieldCommand}
*/
_computeNotification() {
return replace(this.message.notifications[0]);
},
},
fields: {
component: attr(),
dialogOwner: one('Dialog', {
inverse: 'snailmailErrorView',
readonly: true,
required: true,
}),
hasCreditsError: attr({
compute: '_computeHasCreditsError',
}),
message: one('Message', {
compute: '_computeMessage',
required: true,
}),
/**
* Messages from snailmail are considered to have at most one notification.
*/
notification: one('Notification', {
compute: '_computeNotification',
required: true,
}),
},
});