[FIX] mail: fix (inbox reply) composer event handlers (and discard)
task-2092950 closes odoo/odoo#53731 X-original-commit: c29f5776594c4d2278ce163a78d8eb917fb672c0 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com> Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
@@ -7,6 +7,7 @@ const components = {
|
||||
ThreadViewer: require('mail/static/src/components/thread_viewer/thread_viewer.js'),
|
||||
};
|
||||
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
|
||||
const { isEventHandled } = require('mail/static/src/utils/utils.js');
|
||||
|
||||
const { Component } = owl;
|
||||
const { useRef } = owl.hooks;
|
||||
@@ -250,6 +251,10 @@ class ChatWindow extends Component {
|
||||
*/
|
||||
_onFocusinThread(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.chatWindow) {
|
||||
// prevent crash on destroy
|
||||
return;
|
||||
}
|
||||
this.chatWindow.update({ isFocused: true });
|
||||
}
|
||||
|
||||
@@ -293,6 +298,12 @@ class ChatWindow extends Component {
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) {
|
||||
break;
|
||||
}
|
||||
if (isEventHandled(ev, 'Composer.closeEmojisPopover')) {
|
||||
break;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this.chatWindow.close();
|
||||
break;
|
||||
|
||||
@@ -527,8 +527,13 @@ QUnit.test('chat window: open / close', async function (assert) {
|
||||
});
|
||||
|
||||
QUnit.test('chat window: close on ESCAPE', async function (assert) {
|
||||
assert.expect(4);
|
||||
assert.expect(12);
|
||||
|
||||
this.data['res.partner'].records = [{
|
||||
email: "testpartnert@odoo.com",
|
||||
id: 11,
|
||||
name: "TestPartner",
|
||||
}];
|
||||
Object.assign(this.data.initMessaging, {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
@@ -569,12 +574,73 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) {
|
||||
document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click()
|
||||
);
|
||||
assert.verifySteps(['rpc:channel_fold/open']);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_ChatWindow',
|
||||
"chat window should be opened after click on preview"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`.o_Composer_buttonEmojis`).click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_EmojisPopover',
|
||||
"emojis popover should be opened after click on emojis button"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
document.querySelector(`.o_ChatWindow`).focus();
|
||||
const kevt = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
|
||||
document.querySelector(`.o_ChatWindow`).dispatchEvent(kevt);
|
||||
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
|
||||
document.querySelector(`.o_Composer_buttonEmojis`).dispatchEvent(ev);
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_EmojisPopover',
|
||||
"emojis popover should be closed after pressing escape on emojis button"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_ChatWindow',
|
||||
"chat window should still be opened after pressing escape on emojis button"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
document.execCommand('insertText', false, "@");
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`)
|
||||
.dispatchEvent(new window.KeyboardEvent('keydown'));
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`)
|
||||
.dispatchEvent(new window.KeyboardEvent('keyup'));
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_ComposerTextInput_mentionDropdownPropositionList',
|
||||
"mention suggestion should be opened after typing @"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_ComposerTextInput_mentionDropdownPropositionList',
|
||||
"mention suggestion should be closed after pressing escape on mention suggestion"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_ChatWindow',
|
||||
"chat window should still be opened after pressing escape on mention suggestion"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_ChatWindow',
|
||||
"chat window should be closed after pressing escape if there was no other priority escape handler"
|
||||
);
|
||||
assert.verifySteps(['rpc:channel_fold/closed']);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ const components = {
|
||||
};
|
||||
const useDragVisibleDropZone = require('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js');
|
||||
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
|
||||
const {
|
||||
isEventHandled,
|
||||
markEventHandled,
|
||||
} = require('mail/static/src/utils/utils.js');
|
||||
const mailUtils = require('mail.utils');
|
||||
|
||||
const { Component } = owl;
|
||||
@@ -35,8 +39,8 @@ class Composer extends Component {
|
||||
};
|
||||
});
|
||||
/**
|
||||
* Reference of the emoji button. Useful to include emoji popover as
|
||||
* click "inside" the composer for the prop `isDiscardOnClickAway`.
|
||||
* Reference of the emoji popover. Useful to include emoji popover as
|
||||
* contained "inside" the composer.
|
||||
*/
|
||||
this._emojisPopoverRef = useRef('emojisPopover');
|
||||
/**
|
||||
@@ -48,6 +52,7 @@ class Composer extends Component {
|
||||
* Reference of the text input component.
|
||||
*/
|
||||
this._textInputRef = useRef('textInput');
|
||||
|
||||
this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this);
|
||||
}
|
||||
|
||||
@@ -70,6 +75,22 @@ class Composer extends Component {
|
||||
return this.env.models['mail.composer'].get(this.props.composerLocalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given node is self or a children of self, including
|
||||
* the emoji popover.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
contains(node) {
|
||||
// emoji popover is outside but should be considered inside
|
||||
const emojisPopover = this._emojisPopoverRef.comp;
|
||||
if (emojisPopover && emojisPopover.contains(node)) {
|
||||
return true;
|
||||
}
|
||||
return this.el.contains(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current partner image URL.
|
||||
*
|
||||
@@ -172,23 +193,13 @@ class Composer extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the composer when clicking away from the Inbox reply in discuss.
|
||||
* TODO maybe move this in discuss. task-2092950
|
||||
* Discards the composer when clicking away.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickCaptureGlobal(ev) {
|
||||
// in discuss app: prevents discarding the composer when clicking away
|
||||
if (!this.props.isDiscardOnClickAway) {
|
||||
return;
|
||||
}
|
||||
// prevents discarding when clicking on self (on discuss and not on discuss)
|
||||
if (this.el.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
// emoji popover is outside but should be considered inside
|
||||
if (this._emojisPopoverRef.comp.isInsideEventTarget(ev.target)) {
|
||||
if (this.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
this.composer.discard();
|
||||
@@ -280,6 +291,37 @@ class Composer extends Component {
|
||||
this.composer.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onKeydown(ev) {
|
||||
if (ev.key === 'Escape') {
|
||||
if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) {
|
||||
return;
|
||||
}
|
||||
if (isEventHandled(ev, 'Composer.closeEmojisPopover')) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this.composer.discard();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onKeydownEmojiButton(ev) {
|
||||
if (ev.key === 'Escape') {
|
||||
if (this._emojisPopoverRef.comp) {
|
||||
this._emojisPopoverRef.comp.close();
|
||||
this.composer.focus();
|
||||
markEventHandled(ev, 'Composer.closeEmojisPopover');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {CustomEvent} ev
|
||||
@@ -315,7 +357,6 @@ Object.assign(Composer, {
|
||||
hasThreadName: false,
|
||||
hasThreadTyping: false,
|
||||
isCompact: true,
|
||||
isDiscardOnClickAway: false,
|
||||
isExpandable: false,
|
||||
},
|
||||
props: {
|
||||
@@ -357,7 +398,6 @@ Object.assign(Composer, {
|
||||
optional: true,
|
||||
},
|
||||
isCompact: Boolean,
|
||||
isDiscardOnClickAway: Boolean,
|
||||
isExpandable: Boolean,
|
||||
},
|
||||
template: 'mail.Composer',
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
'o-has-header': hasHeader,
|
||||
'o-is-compact': props.isCompact,
|
||||
}"
|
||||
t-on-keydown="_onKeydown"
|
||||
>
|
||||
<t t-if="composer">
|
||||
<t t-if="isDropZoneVisible.value">
|
||||
@@ -77,18 +78,19 @@
|
||||
<div class="o_Composer_toolButtonSeparator"/>
|
||||
</t>
|
||||
<div class="o_Composer_primaryToolButtons" t-att-class="{ 'o-composer-is-compact': props.isCompact }">
|
||||
<Popover position="'top'" t-on-o-emoji-selection="_onEmojiSelection" t-ref="emojisPopover">
|
||||
<Popover position="'top'" t-on-o-emoji-selection="_onEmojiSelection">
|
||||
<!-- TODO FIXME o-open not possible to code due to https://github.com/odoo/owl/issues/693 -->
|
||||
<button class="o_Composer_button o_Composer_buttonEmojis o_Composer_toolButton btn btn-light"
|
||||
t-att-class="{
|
||||
'o-open': false and state.displayed,
|
||||
'o-mobile': env.messaging.device.isMobile,
|
||||
}"
|
||||
t-on-keydown="_onKeydownEmojiButton"
|
||||
>
|
||||
<i class="fa fa-smile-o"/>
|
||||
</button>
|
||||
<t t-set="opened">
|
||||
<EmojisPopover/>
|
||||
<EmojisPopover t-ref="emojisPopover"/>
|
||||
</t>
|
||||
</Popover>
|
||||
<button class="o_Composer_button o_Composer_buttonAttachment o_Composer_toolButton btn btn-light fa fa-paperclip" t-att-class="{ 'o-mobile': env.messaging.device.isMobile }" title="Add attachment" type="button" t-on-click="_onClickAddAttachment"/>
|
||||
|
||||
@@ -266,7 +266,7 @@ QUnit.test('display partner mention suggestions on typing "@"', async function (
|
||||
this.data['res.users'].records = [{
|
||||
partner_id: 11,
|
||||
}];
|
||||
await this.start({});
|
||||
await this.start();
|
||||
const composer = this.env.models['mail.composer'].create();
|
||||
await this.createComposerComponent(composer);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const useStore = require('mail/static/src/component_hooks/use_store/use_store.js
|
||||
const components = {
|
||||
PartnerMentionSuggestion: require('mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js'),
|
||||
};
|
||||
const { markEventHandled } = require('mail/static/src/utils/utils.js');
|
||||
|
||||
const { Component } = owl;
|
||||
const { useRef } = owl.hooks;
|
||||
@@ -206,6 +207,13 @@ class ComposerTextInput extends Component {
|
||||
*/
|
||||
_onKeydownTextarea(ev) {
|
||||
switch (ev.key) {
|
||||
case 'Escape':
|
||||
if (this.composer.hasSuggestedPartners) {
|
||||
ev.preventDefault();
|
||||
this.composer.closeMentionSuggestions();
|
||||
markEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions');
|
||||
}
|
||||
break;
|
||||
// UP, DOWN, TAB: prevent moving cursor if navigation in mention suggestions
|
||||
case 'ArrowUp':
|
||||
case 'PageUp':
|
||||
@@ -256,9 +264,8 @@ class ComposerTextInput extends Component {
|
||||
*/
|
||||
_onKeyupTextarea(ev) {
|
||||
switch (ev.key) {
|
||||
// ESCAPE: close mention suggestions
|
||||
case 'Escape':
|
||||
this._onKeyupTextareaEscape(ev);
|
||||
// already handled in _onKeydownTextarea, break to avoid default
|
||||
break;
|
||||
// ENTER, HOME, END, UP, DOWN, PAGE UP, PAGE DOWN, TAB: check if navigation in mention suggestions
|
||||
case 'Enter':
|
||||
@@ -308,21 +315,6 @@ class ComposerTextInput extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onKeyupTextareaEscape(ev) {
|
||||
if (this.composer.hasSuggestedPartners) {
|
||||
this.composer.closeMentionSuggestions();
|
||||
} else {
|
||||
if (!this._isEmpty()) {
|
||||
return;
|
||||
}
|
||||
this.composer.discard();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
hasCurrentPartnerAvatar="!env.messaging.device.isMobile"
|
||||
hasDiscardButton="true"
|
||||
hasThreadName="true"
|
||||
isDiscardOnClickAway="true"
|
||||
t-on-o-message-posted="_onReplyingToMessageMessagePosted"
|
||||
/>
|
||||
</t>
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
odoo.define('mail/static/src/components/discuss/tests/discuss_inbox_tests.js', function (require) {
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
afterEach: utilsAfterEach,
|
||||
afterNextRender,
|
||||
beforeEach: utilsBeforeEach,
|
||||
nextAnimationFrame,
|
||||
start: utilsStart,
|
||||
} = require('mail/static/src/utils/test_utils.js');
|
||||
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
QUnit.module('components', {}, function () {
|
||||
QUnit.module('discuss', {}, function () {
|
||||
QUnit.module('discuss_inbox_tests.js', {
|
||||
beforeEach() {
|
||||
utilsBeforeEach(this);
|
||||
|
||||
this.start = async params => {
|
||||
let { env, widget } = await utilsStart(Object.assign({}, params, {
|
||||
autoOpenDiscuss: true,
|
||||
data: this.data,
|
||||
hasDiscuss: true,
|
||||
}));
|
||||
this.env = env;
|
||||
this.widget = widget;
|
||||
};
|
||||
},
|
||||
afterEach() {
|
||||
if (this.widget) {
|
||||
this.widget.destroy();
|
||||
}
|
||||
utilsAfterEach(this);
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('reply: discard on pressing escape', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
this.data['res.partner'].records = [{
|
||||
email: "testpartnert@odoo.com",
|
||||
id: 11,
|
||||
name: "TestPartner",
|
||||
}];
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [7, "Demo"],
|
||||
body: "<p>Test</p>",
|
||||
date: "2019-04-20 11:00:00",
|
||||
id: 100,
|
||||
message_type: 'comment',
|
||||
needaction: true,
|
||||
needaction_partner_ids: [3],
|
||||
model: 'project.task',
|
||||
record_name: 'Refactoring',
|
||||
res_id: 20,
|
||||
}];
|
||||
await this.start();
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"should have composer after clicking on reply to message"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`.o_Composer_buttonEmojis`).click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_EmojisPopover',
|
||||
"emojis popover should be opened after click on emojis button"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
|
||||
document.querySelector(`.o_Composer_buttonEmojis`).dispatchEvent(ev);
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_EmojisPopover',
|
||||
"emojis popover should be closed after pressing escape on emojis button"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"reply composer should still be opened after pressing escape on emojis button"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
document.execCommand('insertText', false, "@");
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`)
|
||||
.dispatchEvent(new window.KeyboardEvent('keydown'));
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`)
|
||||
.dispatchEvent(new window.KeyboardEvent('keyup'));
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_ComposerTextInput_mentionDropdownPropositionList',
|
||||
"mention suggestion should be opened after typing @"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_ComposerTextInput_mentionDropdownPropositionList',
|
||||
"mention suggestion should be closed after pressing escape on mention suggestion"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"reply composer should still be opened after pressing escape on mention suggestion"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"reply composer should be closed after pressing escape if there was no other priority escape handler"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('reply: discard on discard button click', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [7, "Demo"],
|
||||
body: "<p>Test</p>",
|
||||
date: "2019-04-20 11:00:00",
|
||||
id: 100,
|
||||
message_type: 'comment',
|
||||
needaction: true,
|
||||
needaction_partner_ids: [3],
|
||||
model: 'project.task',
|
||||
record_name: 'Refactoring',
|
||||
res_id: 20,
|
||||
}];
|
||||
await this.start();
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"should have composer after clicking on reply to message"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer_buttonDiscard',
|
||||
"composer should have a discard button"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`.o_Composer_buttonDiscard`).click()
|
||||
);
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"reply composer should be closed after clicking on discard"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('reply: discard on reply button toggle', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [7, "Demo"],
|
||||
body: "<p>Test</p>",
|
||||
date: "2019-04-20 11:00:00",
|
||||
id: 100,
|
||||
message_type: 'comment',
|
||||
needaction: true,
|
||||
needaction_partner_ids: [3],
|
||||
model: 'project.task',
|
||||
record_name: 'Refactoring',
|
||||
res_id: 20,
|
||||
}];
|
||||
await this.start();
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"should have composer after clicking on reply to message"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`.o_Message_commandReply`).click()
|
||||
);
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"reply composer should be closed after clicking on reply button again"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('reply: discard on click away', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [7, "Demo"],
|
||||
body: "<p>Test</p>",
|
||||
date: "2019-04-20 11:00:00",
|
||||
id: 100,
|
||||
message_type: 'comment',
|
||||
needaction: true,
|
||||
needaction_partner_ids: [3],
|
||||
model: 'project.task',
|
||||
record_name: 'Refactoring',
|
||||
res_id: 20,
|
||||
}];
|
||||
await this.start();
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"should have composer after clicking on reply to message"
|
||||
);
|
||||
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).click();
|
||||
await nextAnimationFrame(); // wait just in case, but nothing is supposed to happen
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"reply composer should still be there after clicking inside itself"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`.o_Composer_buttonEmojis`).click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_EmojisPopover',
|
||||
"emojis popover should be opened after clicking on emojis button"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
document.querySelector(`.o_EmojisPopover_emoji`).click();
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_EmojisPopover',
|
||||
"emojis popover should be closed after selecting an emoji"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"reply composer should still be there after selecting an emoji (even though it is technically a click away, it should be considered inside)"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`.o_Message`).click()
|
||||
);
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"reply composer should be closed after clicking away"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -15,6 +15,27 @@ class EmojisPopover extends Component {
|
||||
this.emojis = emojis;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
close() {
|
||||
this.trigger('o-popover-close');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given node is self or a children of self.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
contains(node) {
|
||||
if (!this.el) {
|
||||
return false;
|
||||
}
|
||||
return this.el.contains(node);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -24,7 +45,7 @@ class EmojisPopover extends Component {
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickEmoji(ev) {
|
||||
this.trigger('o-popover-close');
|
||||
this.close();
|
||||
this.trigger('o-emoji-selection', {
|
||||
unicode: ev.currentTarget.dataset.unicode,
|
||||
});
|
||||
|
||||
@@ -78,6 +78,10 @@ class Message extends Component {
|
||||
notifications: 1,
|
||||
},
|
||||
});
|
||||
/**
|
||||
* The intent of the reply button depends on the last rendered state.
|
||||
*/
|
||||
this._wasSelected;
|
||||
/**
|
||||
* Reference to the content of the message.
|
||||
*/
|
||||
@@ -399,6 +403,7 @@ class Message extends Component {
|
||||
* @private
|
||||
*/
|
||||
_update() {
|
||||
this._wasSelected = this.props.isSelected;
|
||||
if (!this.state.timeElapsed) {
|
||||
this.state.timeElapsed = timeFromNow(this.message.date);
|
||||
}
|
||||
@@ -549,8 +554,15 @@ class Message extends Component {
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickReply(ev) {
|
||||
ev.stopPropagation();
|
||||
this.message.replyTo();
|
||||
// Use this._wasSelected because this.props.isSelected might be changed
|
||||
// by a global capture click handler (for example the one from Composer)
|
||||
// before the current handler is executed. Indeed because it does a
|
||||
// toggle it needs to take into account the value before the click.
|
||||
if (this._wasSelected) {
|
||||
this.env.messaging.discuss.clearReplyingToMessage();
|
||||
} else {
|
||||
this.message.replyTo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -89,7 +89,7 @@ class ThreadViewer extends Component {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async _update() {
|
||||
_update() {
|
||||
const messageList = this._messageListRef.comp;
|
||||
this.trigger('o-rendered');
|
||||
/**
|
||||
|
||||
@@ -294,7 +294,7 @@ function factory(dependencies) {
|
||||
* chat window to put initially on mount.
|
||||
*/
|
||||
threadInitialScrollTop: attr(),
|
||||
thread: one2one('mail.thread', {
|
||||
thread: many2one('mail.thread', {
|
||||
related: 'threadViewer.thread',
|
||||
}),
|
||||
threadDisplayName: attr({
|
||||
|
||||
@@ -27,17 +27,14 @@ function factory(dependencies) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the composer, which only makes sense if the composer is
|
||||
* currently used as a Discuss Inbox reply composer.
|
||||
*/
|
||||
discard() {
|
||||
const discuss = this.env.messaging.discuss;
|
||||
const thread = this.thread;
|
||||
if (
|
||||
!discuss.isOpen ||
|
||||
discuss.thread !== thread ||
|
||||
!discuss.isReplyingToMessage
|
||||
) {
|
||||
return;
|
||||
if (this.discussAsReplying) {
|
||||
this.discussAsReplying.clearReplyingToMessage();
|
||||
}
|
||||
discuss.clearReplyingToMessage();
|
||||
}
|
||||
|
||||
focus() {
|
||||
@@ -509,6 +506,14 @@ function factory(dependencies) {
|
||||
],
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* Instance of discuss if this composer is used as the reply composer
|
||||
* from Inbox. This field is computed from the inverse relation and
|
||||
* should be considered read-only.
|
||||
*/
|
||||
discussAsReplying: one2one('mail.discuss', {
|
||||
inverse: 'replyingToMessageOriginThreadComposer',
|
||||
}),
|
||||
extraSuggestedPartners: many2many('mail.partner', {
|
||||
compute: '_computeExtraSuggestedPartners',
|
||||
dependencies: [
|
||||
|
||||
@@ -176,11 +176,22 @@ function factory(dependencies) {
|
||||
this.update({ renamingThreads: [['unlink', thread]] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to initiate reply to given message in Inbox. Assumes that
|
||||
* Discuss and Inbox are already opened.
|
||||
*
|
||||
* @param {mail.message} message
|
||||
*/
|
||||
replyToMessage(message) {
|
||||
this.update({ replyingToMessage: [['link', message]] });
|
||||
this.replyingToMessageOriginThreadComposer.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {mail.thread} thread
|
||||
*/
|
||||
setThreadRenaming(thread) {
|
||||
this.update({ renamingThreads: [['link', thread ]] });
|
||||
this.update({ renamingThreads: [['link', thread]] });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,6 +273,8 @@ function factory(dependencies) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the reply feature is disabled if discuss is not open.
|
||||
*
|
||||
* @private
|
||||
* @returns {mail.message|undefined}
|
||||
*/
|
||||
@@ -400,9 +413,32 @@ function factory(dependencies) {
|
||||
],
|
||||
}),
|
||||
renamingThreads: one2many('mail.thread'),
|
||||
replyingToMessage: one2one('mail.message', {
|
||||
/**
|
||||
* The message that is currently selected as being replied to in Inbox.
|
||||
* There is only one reply composer shown at a time, which depends on
|
||||
* this selected message.
|
||||
*/
|
||||
replyingToMessage: many2one('mail.message', {
|
||||
compute: '_computeReplyingToMessage',
|
||||
dependencies: ['isOpen'],
|
||||
dependencies: [
|
||||
'isOpen',
|
||||
'replyingToMessage',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* The thread concerned by the reply feature in Inbox. It depends on the
|
||||
* message set to be replied, and should be considered read-only.
|
||||
*/
|
||||
replyingToMessageOriginThread: many2one('mail.thread', {
|
||||
related: 'replyingToMessage.originThread',
|
||||
}),
|
||||
/**
|
||||
* The composer to display for the reply feature in Inbox. It depends
|
||||
* on the message set to be replied, and should be considered read-only.
|
||||
*/
|
||||
replyingToMessageOriginThreadComposer: one2one('mail.composer', {
|
||||
inverse: 'discussAsReplying',
|
||||
related: 'replyingToMessageOriginThread.composer',
|
||||
}),
|
||||
/**
|
||||
* Quick search input value in the discuss sidebar (desktop). Useful
|
||||
|
||||
@@ -277,18 +277,11 @@ function factory(dependencies) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to initiate reply to given message.
|
||||
* Action to initiate reply to current message in Discuss Inbox. Assumes
|
||||
* that Discuss and Inbox are already opened.
|
||||
*/
|
||||
replyTo() {
|
||||
const discuss = this.env.messaging.discuss;
|
||||
if (!discuss.isOpen) {
|
||||
return;
|
||||
}
|
||||
if (discuss.replyingToMessage === this) {
|
||||
discuss.clearReplyingToMessage();
|
||||
} else {
|
||||
discuss.update({ replyingToMessage: [['link', this]] });
|
||||
}
|
||||
this.env.messaging.discuss.replyToMessage(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,36 @@ const {
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const classPatchMap = new WeakMap();
|
||||
const eventHandledWeakMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* Returns whether the given event has been handled with the given markName.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @param {string} markName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isEventHandled(ev, markName) {
|
||||
if (!eventHandledWeakMap.get(ev)) {
|
||||
return false;
|
||||
}
|
||||
return eventHandledWeakMap.get(ev).includes(markName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given event as handled by the given markName. Useful to allow
|
||||
* handlers in the propagation chain to make a decision based on what has
|
||||
* already been done.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @param {string} markName
|
||||
*/
|
||||
function markEventHandled(ev, markName) {
|
||||
if (!eventHandledWeakMap.get(ev)) {
|
||||
eventHandledWeakMap.set(ev, []);
|
||||
}
|
||||
eventHandledWeakMap.get(ev).push(markName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait a task tick, so that anything in micro-task queue that can be processed
|
||||
@@ -117,6 +147,8 @@ function unpatchInstanceMethods(Class, patchName) {
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
isEventHandled,
|
||||
markEventHandled,
|
||||
nextTick,
|
||||
patchClassMethods,
|
||||
patchInstanceMethods,
|
||||
|
||||
@@ -244,6 +244,7 @@
|
||||
<script type="text/javascript" src="/mail/static/src/components/chatter/chatter_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/composer/composer_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_inbox_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_moderation_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_pinned_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_tests.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user