summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorXavier Dubuc <xdu@odoo.com>2020-08-18 12:24:10 +0000
committerAlexandre Kühn <aku@odoo.com>2020-08-21 12:57:44 +0000
commit0263acee7f355e7367686a461c21f28ea34cfa9a (patch)
tree5f445f8b559c46263d438442b505f629d9545015 /addons
parentf25b730b2f548616d62cf8f30907103116e8c0fd (diff)
[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>
Diffstat (limited to 'addons')
-rw-r--r--addons/mail/static/src/components/composer/composer.js18
-rw-r--r--addons/mail/static/src/components/composer/composer.xml2
-rw-r--r--addons/mail/static/src/components/composer/composer_tests.js143
-rw-r--r--addons/mail/static/src/models/composer/composer.js35
-rw-r--r--addons/mail/static/src/utils/test_utils.js1
-rw-r--r--addons/web/static/tests/helpers/test_utils_mock.js14
6 files changed, 203 insertions, 10 deletions
diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js
index 5c3968a0fae..afb7d811eae 100644
--- a/addons/mail/static/src/components/composer/composer.js
+++ b/addons/mail/static/src/components/composer/composer.js
@@ -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();
}
diff --git a/addons/mail/static/src/components/composer/composer.xml b/addons/mail/static/src/components/composer/composer.xml
index 5a7a2e72345..30053cdd0f8 100644
--- a/addons/mail/static/src/components/composer/composer.xml
+++ b/addons/mail/static/src/components/composer/composer.xml
@@ -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"
>
diff --git a/addons/mail/static/src/components/composer/composer_tests.js b/addons/mail/static/src/components/composer/composer_tests.js
index 527003b6dec..8e07affcccb 100644
--- a/addons/mail/static/src/components/composer/composer_tests.js
+++ b/addons/mail/static/src/components/composer/composer_tests.js
@@ -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)"
+ );
+});
+
});
});
});
diff --git a/addons/mail/static/src/models/composer/composer.js b/addons/mail/static/src/models/composer/composer.js
index f2d376fd14b..421ca3061d2 100644
--- a/addons/mail/static/src/models/composer/composer.js
+++ b/addons/mail/static/src/models/composer/composer.js
@@ -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;
}
/**
@@ -320,6 +323,14 @@ function factory(dependencies) {
}
/**
+ * @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,
}),
diff --git a/addons/mail/static/src/utils/test_utils.js b/addons/mail/static/src/utils/test_utils.js
index d42c1318dfa..dfad584dfeb 100644
--- a/addons/mail/static/src/utils/test_utils.js
+++ b/addons/mail/static/src/utils/test_utils.js
@@ -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.
diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js
index 06f9ce33d8a..962b9610f4b 100644
--- a/addons/web/static/tests/helpers/test_utils_mock.js
+++ b/addons/web/static/tests/helpers/test_utils_mock.js
@@ -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,