[FIX] mail: prevent composer to send message if pending uploads
task-2317862 closes odoo/odoo#56303 X-original-commit: 3b5cd048f1dfa18869e95b173b4232b4bb189025 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
committed by
Alexandre Kühn
parent
f25b730b2f
commit
0263acee7f
@@ -175,9 +175,21 @@ class Composer extends Component {
|
||||
/**
|
||||
* Post a message in the composer on related thread.
|
||||
*
|
||||
* Posting of the message could be aborted if it cannot be posted like if there are attachments
|
||||
* currently uploading or if there is no text content and no attachments.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async _postMessage() {
|
||||
if (!this.composer.canPostMessage) {
|
||||
if (this.composer.hasUploadingAttachment) {
|
||||
this.env.services['notification'].notify({
|
||||
message: this.env._t("Please wait while the file is uploading."),
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// TODO: take suggested recipients into account (task-2283356)
|
||||
await this.composer.postMessage();
|
||||
// TODO: we might need to remove trigger and use the store to wait for the post rpc to be done
|
||||
@@ -246,9 +258,6 @@ class Composer extends Component {
|
||||
* @private
|
||||
*/
|
||||
_onClickSend() {
|
||||
if (!this.composer.canPostMessage) {
|
||||
return;
|
||||
}
|
||||
this._postMessage();
|
||||
}
|
||||
|
||||
@@ -335,9 +344,6 @@ class Composer extends Component {
|
||||
* @private
|
||||
*/
|
||||
_onTextInputKeydownEnter() {
|
||||
if (!this.composer.canPostMessage) {
|
||||
return;
|
||||
}
|
||||
this._postMessage();
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
'o-composer-is-compact': props.isCompact,
|
||||
'o-has-current-partner-avatar': props.hasCurrentPartnerAvatar,
|
||||
}"
|
||||
t-att-disabled="(!composer.textInputContent and composer.attachments.length === 0) ? 'disabled' : ''"
|
||||
t-att-disabled="!composer.canPostMessage ? 'disabled' : ''"
|
||||
type="button"
|
||||
t-on-click="_onClickSend"
|
||||
>
|
||||
|
||||
@@ -21,6 +21,7 @@ const {
|
||||
createFile,
|
||||
inputFiles,
|
||||
},
|
||||
makeTestPromise,
|
||||
} = require('web.test_utils');
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
@@ -886,6 +887,148 @@ QUnit.test('current partner notify is typing again to other members every 50s of
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('composer: send button is disabled if attachment upload is not finished', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const attachmentUploadedPromise = makeTestPromise();
|
||||
await this.start({
|
||||
async mockFetch(resource, init) {
|
||||
const res = this._super(...arguments);
|
||||
if (resource === '/web/binary/upload_attachment') {
|
||||
await attachmentUploadedPromise;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
});
|
||||
const composer = this.env.models['mail.composer'].create();
|
||||
await this.createComposerComponent(composer);
|
||||
const file = await createFile({
|
||||
content: 'hello, world',
|
||||
contentType: 'text/plain',
|
||||
name: 'text.txt',
|
||||
});
|
||||
await afterNextRender(() =>
|
||||
inputFiles(
|
||||
document.querySelector('.o_FileUploader_input'),
|
||||
[file]
|
||||
)
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Attachment',
|
||||
"should have an attachment after a file has been input"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Attachment.o-temporary',
|
||||
"attachment displayed is being uploaded"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer_buttonSend',
|
||||
"composer send button should be displayed"
|
||||
);
|
||||
assert.ok(
|
||||
!!document.querySelector('.o_Composer_buttonSend').attributes.disabled,
|
||||
"composer send button should be disabled as attachment is not yet uploaded"
|
||||
);
|
||||
|
||||
// simulates attachment finishes uploading
|
||||
await afterNextRender(() => attachmentUploadedPromise.resolve());
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Attachment',
|
||||
"should have only one attachment"
|
||||
);
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_Attachment.o-temporary',
|
||||
"attachment displayed should be uploaded"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer_buttonSend',
|
||||
"composer send button should still be present"
|
||||
);
|
||||
assert.ok(
|
||||
!document.querySelector('.o_Composer_buttonSend').attributes.disabled,
|
||||
"composer send button should be enabled as attachment is now uploaded"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('warning when attempting to post message when attachments are still uploading', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
await this.start({
|
||||
async mockFetch(resource, init) {
|
||||
const res = this._super(...arguments);
|
||||
if (resource === '/web/binary/upload_attachment') {
|
||||
// simulates attachment is never finished uploading
|
||||
await new Promise(() => {});
|
||||
}
|
||||
return res;
|
||||
},
|
||||
services: {
|
||||
notification: {
|
||||
notify(params) {
|
||||
assert.strictEqual(
|
||||
params.message,
|
||||
"Please wait while the file is uploading.",
|
||||
"notification content should be about the uploading file"
|
||||
);
|
||||
assert.strictEqual(
|
||||
params.type,
|
||||
'warning',
|
||||
"notification should be a warning"
|
||||
);
|
||||
assert.step('notification');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
const thread = this.env.models['mail.thread'].create({
|
||||
composer: [['create', { isLog: false }]],
|
||||
id: 20,
|
||||
model: 'res.partner',
|
||||
});
|
||||
await this.createComposerComponent(thread.composer);
|
||||
const file = await createFile({
|
||||
content: 'hello, world',
|
||||
contentType: 'text/plain',
|
||||
name: 'text.txt',
|
||||
});
|
||||
await afterNextRender(() =>
|
||||
inputFiles(
|
||||
document.querySelector('.o_FileUploader_input'),
|
||||
[file]
|
||||
)
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Attachment',
|
||||
"should have only one attachment"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Attachment.o-temporary',
|
||||
"attachment displayed is being uploaded"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer_buttonSend',
|
||||
"composer send button should be displayed"
|
||||
);
|
||||
|
||||
// Try to send message
|
||||
document
|
||||
.querySelector(`.o_ComposerTextInput_textarea`)
|
||||
.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter' }));
|
||||
assert.verifySteps(
|
||||
['notification'],
|
||||
"should have triggered a notification for inability to post message at the moment (some attachments are still being uploaded)"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,7 +294,10 @@ function factory(dependencies) {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeCanPostMessage() {
|
||||
return this.textInputContent || this.attachments.length !== 0;
|
||||
if (!this.textInputContent && this.attachments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return !this.hasUploadingAttachment;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,6 +322,14 @@ function factory(dependencies) {
|
||||
return this.allSuggestedPartners.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeHasUploadingAttachment() {
|
||||
return this.attachments.some(attachment => attachment.isTemporary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if mentioned partners are still in the composer text input content
|
||||
* and removes them if not.
|
||||
@@ -540,10 +551,21 @@ function factory(dependencies) {
|
||||
attachments: many2many('mail.attachment', {
|
||||
inverse: 'composers',
|
||||
}),
|
||||
/**
|
||||
* This field watches the uploading (= temporary) status of attachments
|
||||
* linked to this composer.
|
||||
*
|
||||
* Useful to determine whether there are some attachments that are being
|
||||
* uploaded.
|
||||
*/
|
||||
attachmentsAreTemporary: attr({
|
||||
related: 'attachments.isTemporary',
|
||||
}),
|
||||
canPostMessage: attr({
|
||||
compute: '_computeCanPostMessage',
|
||||
dependencies: [
|
||||
'attachments',
|
||||
'hasUploadingAttachment',
|
||||
'textInputContent',
|
||||
],
|
||||
default: false,
|
||||
@@ -563,6 +585,17 @@ function factory(dependencies) {
|
||||
'mainSuggestedPartners',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* This field determines whether some attachments linked to this
|
||||
* composer are being uploaded.
|
||||
*/
|
||||
hasUploadingAttachment: attr({
|
||||
compute: '_computeHasUploadingAttachment',
|
||||
dependencies: [
|
||||
'attachments',
|
||||
'attachmentsAreTemporary',
|
||||
],
|
||||
}),
|
||||
hasFocus: attr({
|
||||
default: false,
|
||||
}),
|
||||
|
||||
@@ -368,6 +368,7 @@ async function createRootComponent(self, Component, { props = {}, target }) {
|
||||
* is set: provide data that is passed to discuss widget (= client action) as
|
||||
* 2nd positional argument.
|
||||
* @param {Object} [param0.env={}]
|
||||
* @param {function} [param0.mockFetch]
|
||||
* @param {function} [param0.mockRPC]
|
||||
* @param {boolean} [param0.hasActionManager=false] if set, use
|
||||
* createActionManager.
|
||||
|
||||
@@ -333,6 +333,7 @@ function removeSrcAttribute(el, rpc) {
|
||||
* @param {string} [params.currentDate]
|
||||
* @param {Object} [params.data]
|
||||
* @param {boolean} [params.debug]
|
||||
* @param {function} [params.mockFetch]
|
||||
* @param {function} [params.mockRPC]
|
||||
* @param {number} [params.fieldDebounce=0] the value of the DEBOUNCE attribute
|
||||
* of fields
|
||||
@@ -351,8 +352,11 @@ async function addMockEnvironmentOwl(Component, params, mockServer) {
|
||||
// instantiate a mockServer if not provided
|
||||
if (!mockServer) {
|
||||
let Server = MockServer;
|
||||
if (params.mockFetch) {
|
||||
Server = MockServer.extend({ _performFetch: params.mockFetch });
|
||||
}
|
||||
if (params.mockRPC) {
|
||||
Server = MockServer.extend({ _performRpc: params.mockRPC });
|
||||
Server = Server.extend({ _performRpc: params.mockRPC });
|
||||
}
|
||||
mockServer = new Server(params.data, {
|
||||
actions: params.actions,
|
||||
@@ -466,6 +470,9 @@ async function addMockEnvironmentOwl(Component, params, mockServer) {
|
||||
* @param {number} [params.debug] if set to true, logs RPCs and uncaught Odoo
|
||||
* events.
|
||||
* @param {Object} [params.bus] the instance of Bus that will be used (in the env)
|
||||
* @param {function} [params.mockFetch] a function that will be used to override
|
||||
* the _performFetch method from the mock server. It is really useful to add
|
||||
* some custom fetch mocks, or to check some assertions.
|
||||
* @param {function} [params.mockRPC] a function that will be used to override
|
||||
* the _performRpc method from the mock server. It is really useful to add
|
||||
* some custom rpc mocks, or to check some assertions.
|
||||
@@ -506,8 +513,11 @@ async function addMockEnvironment(widget, params) {
|
||||
|
||||
// instantiate mock server
|
||||
var Server = MockServer;
|
||||
if (params.mockFetch) {
|
||||
Server = MockServer.extend({ _performFetch: params.mockFetch });
|
||||
}
|
||||
if (params.mockRPC) {
|
||||
Server = MockServer.extend({ _performRpc: params.mockRPC });
|
||||
Server = Server.extend({ _performRpc: params.mockRPC });
|
||||
}
|
||||
var mockServer = new Server(params.data, {
|
||||
actions: params.actions,
|
||||
|
||||
Reference in New Issue
Block a user