summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOdoo's Mergebot <xmo+roboto@odoo.com>2019-09-03 13:06:23 +0200
committerGitHub <noreply@github.com>2019-09-03 13:06:23 +0200
commit85c5d0a4be4bf7afb7d15f0b5a07cc0daecaa1a5 (patch)
treecb11f94d3c993e83e93a386914075d9e8e8a3cde
parent82c0e9b620bfbd409914e1941e5c8e9117982bd1 (diff)
parent40b57ca824e967a9221f7834e4c9b3db7a4849d0 (diff)
[MERGE][FIX] bus, mail, web: several cross-tab communication fixes
### [FIX] bus: background slave tabs should not loose notifications Slave tabs missed some notifications in case the local storage event's `newValue` did not match actual value in local storage. This happens when master tab overwrites a value that slave tabs did not take the time to read. Slave tabs should now always handle all local storage events. Task-ID 2044231 ### [FIX] bus, mail: correctly synchronize document chat windows Cross-tab synchronization of document chat windows resulted sometimes with an infinite loop. This happens due to tabs relying on actual value in the local storage and all document chat windows state being set as a single local storage entry. Cross-tab communication of document chat windows now is relying on event's `newValue`, and each document chat window has its own local storage entry. Changes from local storage no longer writes again in the local storage, so that it no longer makes an echo (= infinite loop). Task-Id 2034997 closes odoo/odoo#36177 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
-rw-r--r--addons/bus/static/src/js/crosstab_bus.js6
-rw-r--r--addons/mail/static/src/js/models/threads/document_thread.js27
-rw-r--r--addons/mail/static/src/js/services/mail_document_thread_manager.js73
-rw-r--r--addons/mail/static/src/js/services/mail_window_manager.js6
-rw-r--r--addons/mail/static/tests/document_thread_window_tests.js20
-rw-r--r--addons/web/static/src/js/core/abstract_storage_service.js13
-rw-r--r--addons/web/static/src/js/core/local_storage.js10
-rw-r--r--addons/web/static/src/js/core/ram_storage.js14
8 files changed, 130 insertions, 39 deletions
diff --git a/addons/bus/static/src/js/crosstab_bus.js b/addons/bus/static/src/js/crosstab_bus.js
index 46643cad0de..9d0e413ab94 100644
--- a/addons/bus/static/src/js/crosstab_bus.js
+++ b/addons/bus/static/src/js/crosstab_bus.js
@@ -80,6 +80,12 @@ var CrossTabBus = Longpolling.extend({
this._callLocalStorage('setItem', 'channels', this._channels);
},
/**
+ * @return {string}
+ */
+ getTabId: function () {
+ return this._id;
+ },
+ /**
* Tells whether this bus is related to the master tab.
*
* @returns {boolean}
diff --git a/addons/mail/static/src/js/models/threads/document_thread.js b/addons/mail/static/src/js/models/threads/document_thread.js
index 18f1e4fb762..3d91b786cef 100644
--- a/addons/mail/static/src/js/models/threads/document_thread.js
+++ b/addons/mail/static/src/js/models/threads/document_thread.js
@@ -62,9 +62,16 @@ var DocumentThread = Thread.extend({
* shared between tabs, and restored on F5.
*
* @override
+ * @param {Object} [options]
+ * @param {boolean} [options.skipCrossTabSync=false] if set, it should
+ * not notify other tabs from updating document thread state. This happens
+ * in case the `close` operation comes from local storage event.
*/
- close: function () {
+ close: function (options) {
this._super.apply(this, arguments);
+ if (options && options.skipCrossTabSync) {
+ return;
+ }
this.call('mail_service', 'updateDocumentThreadState', this._id, {
name: this.getName(),
windowState: 'closed',
@@ -75,9 +82,16 @@ var DocumentThread = Thread.extend({
* shared between tabs, and restored on F5.
*
* @override
+ * @param {Object} [options]
+ * @param {boolean} [options.skipCrossTabSync=false] if set, it should
+ * not notify other tabs from updating document thread state. This happens
+ * in case the `detach` operation comes from local storage event.
*/
- detach: function () {
+ detach: function (options) {
this._super.apply(this, arguments);
+ if (options && options.skipCrossTabSync) {
+ return;
+ }
var windowState = this._folded ? 'folded' : 'open';
this.call('mail_service', 'updateDocumentThreadState', this._id, {
name: this.getName(),
@@ -105,9 +119,16 @@ var DocumentThread = Thread.extend({
* shared between tabs, and restored on F5.
*
* @override
+ * @param {Object} [options]
+ * @param {boolean} [options.skipCrossTabSync=false] if set, it should
+ * not notify other tabs from updating document thread state. This happens
+ * in case the `detach` operation comes from local storage event.
*/
- fold: function () {
+ fold: function (folded, options) {
this._super.apply(this, arguments);
+ if (options && options.skipCrossTabSync) {
+ return;
+ }
var windowState = this._folded ? 'folded' : 'open';
this.call('mail_service', 'updateDocumentThreadState', this._id, {
name: this.getName(),
diff --git a/addons/mail/static/src/js/services/mail_document_thread_manager.js b/addons/mail/static/src/js/services/mail_document_thread_manager.js
index 000a1c15fa5..1af3fc337a3 100644
--- a/addons/mail/static/src/js/services/mail_document_thread_manager.js
+++ b/addons/mail/static/src/js/services/mail_document_thread_manager.js
@@ -21,17 +21,23 @@ MailManager.include({
start: function () {
this._super.apply(this, arguments);
-
+ this._documentThreadWindowStates = {};
+ this._tabId = this.call('bus_service', 'getTabId');
// retrieve the open DocumentThreads from the localStorage
- var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
- if (!state) {
- this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, {});
- } else {
- this.isReady().then(this._updateDocumentThreadWindows.bind(this, state));
+ var localStorageLength = this.call('local_storage', 'length');
+ for (var i = 0; i < localStorageLength; i++) {
+ var key = this.call('local_storage', 'key', i);
+ if (key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) {
+ // key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId
+ var documentThreadId = key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1);
+ this._documentThreadWindowStates[documentThreadId] =
+ this.call('local_storage', 'getItem', key);
+ }
}
+ this.isReady().then(this._updateDocumentThreadWindows.bind(this, this._documentThreadWindowStates));
// listen to localStorage changes to synchronize DocumentThread's
// windows between tabs
- window.addEventListener('storage', this._onStorage.bind(this));
+ this.call('local_storage', 'onStorage', this, this._onStorage);
},
//--------------------------------------------------------------------------
@@ -60,7 +66,10 @@ MailManager.include({
options.postedFromDocumentThread
) {
var key = this.DOCUMENT_THREAD_MESSAGE_KEY;
- this.call('local_storage', 'setItem', key, data);
+ this.call('local_storage', 'setItem', key, {
+ messageData: data,
+ tabId: this._tabId,
+ });
}
return this._super.apply(this, arguments);
},
@@ -144,12 +153,11 @@ MailManager.include({
* @param {string} state.windowState ('closed', 'folded' or 'open')
*/
updateDocumentThreadState: function (threadID, state) {
- var states = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
- states = _.omit(states, function (state) {
- return state.windowState === 'closed';
+ this._documentThreadWindowStates[threadID] = state;
+ this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY + '/' + threadID, {
+ state: state,
+ tabId: this._tabId,
});
- states[threadID] = state;
- this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, states);
},
//--------------------------------------------------------------------------
@@ -194,10 +202,17 @@ MailManager.include({
resModel: info[0],
});
if (state.windowState === 'closed') {
- documentThread.close();
+ documentThread.close({
+ skipCrossTabSync: true,
+ });
} else {
- documentThread.fold(state.windowState === 'folded');
- self.openThreadWindow(documentThread.getID(), { keepFoldState: true });
+ documentThread.fold(state.windowState === 'folded', {
+ skipCrossTabSync: true,
+ });
+ self.openThreadWindow(documentThread.getID(), {
+ keepFoldState: true,
+ skipCrossTabSync: true,
+ });
}
});
},
@@ -213,13 +228,27 @@ MailManager.include({
* @param {StorageEvent} ev
*/
_onStorage: function (ev) {
- if (ev.key === this.DOCUMENT_THREAD_STATE_KEY) {
- var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
- this._updateDocumentThreadWindows(state);
+ var value;
+ try {
+ value = JSON.parse(ev.newValue);
+ } catch (err) {
+ return;
+ }
+ if (ev.key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) {
+ if (value.tabId === this._tabId) {
+ return;
+ }
+ // key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId
+ var documentThreadId = ev.key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1);
+ var param = {};
+ param[documentThreadId] = value.state;
+ this._updateDocumentThreadWindows(param);
} else if (ev.key === this.DOCUMENT_THREAD_MESSAGE_KEY) {
- var message = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_MESSAGE_KEY);
- if (message) {
- this.addMessage(message);
+ if (value.tabId === this._tabId) {
+ return;
+ }
+ if (value.messageData) {
+ this.addMessage(value.messageData);
}
}
},
diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js
index ebb6a0ff2e7..c5b9f2566c5 100644
--- a/addons/mail/static/src/js/services/mail_window_manager.js
+++ b/addons/mail/static/src/js/services/mail_window_manager.js
@@ -94,6 +94,8 @@ MailManager.include({
* is not open yet, and do nothing otherwise.
* @param {boolean} [options.keepFoldState=false] if set to true, keep the
* fold state of the thread
+ * @param {boolean} [options.skipCrossTabSync=false] if set, thread
+ * should not notify other tabs from new document thread chat window state.
*/
openThreadWindow: function (threadID, options) {
var self = this;
@@ -130,7 +132,9 @@ MailManager.include({
// thread window could not be open, which may happen due to
// access error while fetching messages to the document.
// abort opening the thread window in this case.
- thread.close();
+ thread.close({
+ skipCrossTabSync: true,
+ });
}).always(function () {
thread.isCreatingWindow = false;
});
diff --git a/addons/mail/static/tests/document_thread_window_tests.js b/addons/mail/static/tests/document_thread_window_tests.js
index bdab46e91ef..fe2b31b2811 100644
--- a/addons/mail/static/tests/document_thread_window_tests.js
+++ b/addons/mail/static/tests/document_thread_window_tests.js
@@ -201,7 +201,7 @@ QUnit.test('post messages in a document thread window', function (assert) {
testUtils.intercept(messagingMenu, 'call_service', function (ev) {
if (ev.data.service === 'local_storage' && ev.data.method === 'setItem' &&
ev.data.args[0] === 'mail.document_threads_last_message') {
- assert.deepEqual(ev.data.args[1], newMessage,
+ assert.deepEqual(ev.data.args[1].messageData, newMessage,
"should write sent message in local storage, to share info with other tabs");
}
}, true);
@@ -229,7 +229,7 @@ QUnit.test('post messages in a document thread window', function (assert) {
});
QUnit.test('open, fold, unfold and close a document thread window', function (assert) {
- assert.expect(8);
+ assert.expect(24);
var messagingMenu = new MessagingMenu();
testUtils.addMockEnvironment(messagingMenu, {
@@ -239,7 +239,13 @@ QUnit.test('open, fold, unfold and close a document thread window', function (as
});
testUtils.intercept(messagingMenu, 'call_service', function (ev) {
if (ev.data.service === 'local_storage' && ev.data.method === 'setItem') {
- assert.step(ev.data.args);
+ const key = ev.data.args[0];
+ const state = ev.data.args[1].state;
+ assert.strictEqual(key, 'mail.document_threads_state/some.res.model_1');
+ assert.ok(state);
+ assert.ok(state.name);
+ assert.ok(state.windowState);
+ assert.step(`${key}: { name: "${state.name}", windowState: '${state.windowState}' }`);
}
}, true);
messagingMenu.appendTo($('#qunit-fixture'));
@@ -262,10 +268,10 @@ QUnit.test('open, fold, unfold and close a document thread window', function (as
$('.o_thread_window .o_thread_window_close').click();
assert.verifySteps([
- ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "open"}}],
- ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "folded"}}],
- ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "open"}}],
- ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "closed"}}],
+ `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`,
+ `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'folded' }`,
+ `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`,
+ `mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'closed' }`,
]);
messagingMenu.destroy();
diff --git a/addons/web/static/src/js/core/abstract_storage_service.js b/addons/web/static/src/js/core/abstract_storage_service.js
index ef828b5dd5c..5d724f87911 100644
--- a/addons/web/static/src/js/core/abstract_storage_service.js
+++ b/addons/web/static/src/js/core/abstract_storage_service.js
@@ -45,6 +45,19 @@ var AbstractStorageService = AbstractService.extend({
return val ? JSON.parse(val) : defaultValue;
},
/**
+ * @param {integer} index
+ * @return {string}
+ */
+ key: function (index) {
+ return this.storage.key(index);
+ },
+ /**
+ * @return {integer}
+ */
+ length: function () {
+ return this.storage.length;
+ },
+ /**
* Removes the given key from the storage
*
* @param {string} key
diff --git a/addons/web/static/src/js/core/local_storage.js b/addons/web/static/src/js/core/local_storage.js
index 0ae98498b31..ecc0cb7b78f 100644
--- a/addons/web/static/src/js/core/local_storage.js
+++ b/addons/web/static/src/js/core/local_storage.js
@@ -35,12 +35,10 @@ try {
var newValue = e.originalEvent.newValue;
try {
JSON.parse(newValue);
- if (localStorage.getItem(key) === newValue) {
- storage.trigger('storage', {
- key: key,
- newValue: newValue,
- });
- }
+ storage.trigger('storage', {
+ key: key,
+ newValue: newValue,
+ });
} catch (error) {}
});
return storage;
diff --git a/addons/web/static/src/js/core/ram_storage.js b/addons/web/static/src/js/core/ram_storage.js
index 9bf0fc29f93..abad86c4538 100644
--- a/addons/web/static/src/js/core/ram_storage.js
+++ b/addons/web/static/src/js/core/ram_storage.js
@@ -31,6 +31,7 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
*/
clear: function () {
this.storage = Object.create(null);
+ this.length = 0;
},
/**
* Returns the value associated with a given key in the storage
@@ -42,11 +43,21 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
return this.storage[key];
},
/**
+ * @param {integer} index
+ * @return {string}
+ */
+ key: function (index) {
+ return _.keys(this.storage)[index];
+ },
+ /**
* Removes the given key from the storage
*
* @param {string} key
*/
removeItem: function (key) {
+ if (key in this.storage) {
+ this.length--;
+ }
delete this.storage[key];
this.trigger('storage', {key: key, newValue: null});
},
@@ -58,6 +69,9 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
* @param {string} value
*/
setItem: function (key, value) {
+ if (!(key in this.storage)) {
+ this.length++;
+ }
this.storage[key] = value;
this.trigger('storage', {key: key, newValue: value});
},