diff --git a/addons/bus/static/src/js/bus.js b/addons/bus/static/src/js/bus.js
deleted file mode 100644
index bf5bb5277be..00000000000
--- a/addons/bus/static/src/js/bus.js
+++ /dev/null
@@ -1,485 +0,0 @@
-odoo.define('bus.bus', function (require) {
-"use strict";
-
-var local_storage = require('web.local_storage');
-var session = require('web.session');
-var Widget = require('web.Widget');
-
-var bus = {};
-var PARTNERS_PRESENCE_CHECK_PERIOD = 30000; // don't check presence more than once every 30s
-
-var TAB_HEARTBEAT_PERIOD = 10000; // 10 seconds
-var MASTER_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 second
-
-bus.ERROR_DELAY = 10000;
-
-bus.Bus = Widget.extend({
- /**
- * @override
- * @param {Object} params
- * @param {Object} params.session the session to use to perform the poll RPC
- * @param {string} params.pollRoute the longpolling route to use
- */
- init: function (params) {
- var self = this;
- this._super();
- this.pollRoute = params.pollRoute;
- this.session = params.session;
- this.options = {};
- this.activated = false;
- this.bus_id = _.uniqueId('bus');
- this.channels = [];
- this.last = 0;
- this.stop = false;
- this.is_master = true;
-
- // bus presence
- this.last_presence = new Date().getTime();
- this.last_partners_presence_check = this.last_presence;
- this.set("window_focus", true);
- this.on("change:window_focus", this, function () {
- if (this.get("window_focus")) {
- this.trigger('window_focus', this.is_master);
- }
- });
- $(window).on("focus." + this.bus_id, _.bind(this.focus_change, this, true));
- $(window).on("blur." + this.bus_id, _.bind(this.focus_change, this, false));
- $(window).on("unload." + this.bus_id, _.bind(this.focus_change, this, false));
- _.each('click,keydown,keyup'.split(','), function (evtype) {
- $(window).on(evtype + "." + self.bus_id, function () {
- self.last_presence = new Date().getTime();
- });
- });
- },
- destroy: function () {
- var self = this;
- this.stop_polling();
- $(window).off("focus." + this.bus_id);
- $(window).off("blur." + this.bus_id);
- $(window).off("unload." + this.bus_id);
- _.each('click,keydown,keyup'.split(','), function (evtype) {
- $(window).off(evtype + "." + self.bus_id);
- });
- },
- start_polling: function () {
- if (!this.activated) {
- this.poll();
- this.stop = false;
- }
- },
- stop_polling: function () {
- this.activated = false;
- this.stop = true;
- this.channels = [];
- if (this._pollRpc) {
- this._pollRpc.abort();
- }
- },
- poll: function () {
- var self = this;
- self.activated = true;
- var now = new Date().getTime();
- var options = _.extend({}, this.options, {
- bus_inactivity: now - this.get_last_presence(),
- });
- if (this.last_partners_presence_check + PARTNERS_PRESENCE_CHECK_PERIOD > now) {
- options = _.omit(options, 'bus_presence_partner_ids');
- } else {
- this.last_partners_presence_check = now;
- }
- var data = {channels: self.channels, last: self.last, options: options};
- // The backend has a maximum cycle time of 50 seconds so give +10 seconds
- this._pollRpc = this.session.rpc(this.pollRoute, data, {shadow : true, timeout: 60000});
- this._pollRpc.then(function (result) {
- self._pollRpc = false;
- self.on_notification(result);
- if (!self.stop && self.activated) {
- self.poll();
- }
- }, function (error, event) {
- self._pollRpc = false;
- // no error popup if request is interrupted or fails for any reason
- event.preventDefault();
- if (error && error.message === "XmlHttpRequestError abort") {
- if (!self.stop && self.activated) {
- self.poll();
- }
- } else {
- // random delay to avoid massive longpolling
- setTimeout(_.bind(self.poll, self), bus.ERROR_DELAY + (Math.floor((Math.random()*20)+1)*1000));
- }
- });
- },
- on_notification: function (notifications) {
- var self = this;
- var notifs = _.map(notifications, function (notif) {
- if (notif.id > self.last) {
- self.last = notif.id;
- }
- return [notif.channel, notif.message];
- });
- this.trigger("notification", notifs);
- },
- add_channel: function (channel) {
- if (this.channels.indexOf(channel) === -1) {
- this.channels.push(channel);
- if (this._pollRpc) {
- this._pollRpc.abort();
- }
- }
- },
- delete_channel: function (channel) {
- var index = this.channels.indexOf(channel);
- if (index !== -1) {
- this.channels.splice(index, 1);
- if (this._pollRpc) {
- this._pollRpc.abort();
- }
- }
- },
- // bus presence : window focus/unfocus
- focus_change: function (focus) {
- this.set("window_focus", focus);
- },
- is_odoo_focused: function () {
- return this.get("window_focus");
- },
- get_last_presence: function () {
- return this.last_presence;
- },
- update_option: function (key, value) {
- this.options[key] = value;
- },
- delete_option: function (key) {
- if (_.contains(_.keys(this.options), key)) {
- delete this.options[key];
- }
- },
-});
-
-
-
-/**
- * CrossTabBus Widget
- *
- * Manage the communication before browser tab to allow only one tab polling for the others (performance improvement)
- * When a tab is opened, and the start_polling method is called, the tab is signaling through the localStorage to the
- * others. When a tab is closed, it signals its removing. If he was the master tab (the polling one), he choose another
- * one in the list of open tabs. This one start polling for the other. When a notification is recieved from the poll, it
- * is signaling through the localStorage too.
- *
- * localStorage used keys are:
- *
- * - bus.channels : shared public channel list to listen during the poll
- * - bus.options : shared options
- * - bus.notification : the received notifications from the last poll
- * - bus.tab_list : list of opened tab ids
- * - bus.tab_master : generated id of the master tab
- */
-bus.CrossTabBus = bus.Bus.extend({
- init: function () {
- this._super.apply(this, arguments);
-
- // used to prefix localStorage keys
- this.sanitizedOrigin = this.session.origin.replace(/:\/{0,2}/g, '_');
-
- this.is_master = false;
- this.is_registered = false;
- if (parseInt(this._getItem('last_ts', 0)) + 50000 < new Date().getTime()) {
- this._setItem('last', -1);
- }
-
- on("storage", this.on_storage.bind(this));
- },
- start_polling: function () {
- var self = this;
- if (!this.is_registered) {
- this.is_registered = true;
- tab_manager.register_tab(function () {
- self.is_master = true;
- self.start_polling();
- }, function () {
- self.is_master = false;
- self.stop_polling();
- }, function () {
- // Write last_presence in local storage if it has been updated since last heartbeat
- var hb_period = this.is_master ? MASTER_TAB_HEARTBEAT_PERIOD : TAB_HEARTBEAT_PERIOD;
- if (self.last_presence + hb_period > new Date().getTime()) {
- self._setItem('last_presence', self.last_presence);
- }
- }, this._generateKey.bind(this));
- if (this.is_master) {
- this._setItem('channels', this.channels);
- this._setItem('options', this.options);
- } else {
- this.channels = this._getItem('channels', this.channels);
- this.options = this._getItem('options', this.options);
- }
- return; // start_polling will be called again on tab registration
- }
-
- if (this.is_master) {
- this._super.apply(this, arguments);
- }
- },
- on_notification: function (notifications) {
- if (this.is_master) { // broadcast to other tabs
- var last = this._getItem('last', -1);
- var max_id = Math.max(last, 0);
- var new_notifications = _.filter(notifications, function (notif) {
- max_id = Math.max(max_id, notif.id);
- return notif.id < 0 || notif.id > last;
- });
- this.last = max_id;
- if (new_notifications.length) {
- this._setItem('last', max_id);
- this._setItem('last_ts', new Date().getTime());
- this._setItem('notification', new_notifications);
- this._super(new_notifications);
- }
- } else {
- this._super.apply(this, arguments);
- }
- },
- on_storage: function (e) {
- // use the value of event to not read from
- // localStorage (avoid race condition)
- var value = e.newValue;
- // notifications changed
- if (e.key === this._generateKey('notification')) {
- var notifs = JSON.parse(value);
- this.on_notification(notifs);
- }
- // update channels
- if (e.key === this._generateKey('channels')) {
- this.channels = JSON.parse(value);
- }
- // update options
- if (e.key === this._generateKey('options')) {
- this.options = JSON.parse(value);
- }
- // update focus
- if (e.key === this._generateKey('focus')) {
- this.set('window_focus', JSON.parse(value));
- }
- },
- add_channel: function () {
- this._super.apply(this, arguments);
- this._setItem('channels', this.channels);
- },
- delete_channel: function () {
- this._super.apply(this, arguments);
- this._setItem('channels', this.channels);
- },
- get_last_presence: function () {
- return this._getItem('last_presence') || new Date().getTime();
- },
- update_option: function () {
- this._super.apply(this, arguments);
- this._setItem('options', this.options);
- },
- delete_option: function () {
- this._super.apply(this, arguments);
- this._setItem('options', this.options);
- },
- focus_change: function (focus) {
- this._super.apply(this, arguments);
- this._setItem('focus', focus);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Generates localStorage keys prefixed by bus. (the name of this addon),
- * and the sanitized origin, to prevent keys from conflicting when several
- * bus instances (polling different origins) co-exist.
- *
- * @private
- * @param {string} key
- * @returns key prefixed with the origin
- */
- _generateKey: function (key) {
- return 'bus.' + this.sanitizedOrigin + '.' + key;
- },
- /**
- * @private
- * @param {string} key
- * @param {*} defaultValue
- */
- _getItem: function (key, defaultValue) {
- return getItem(this._generateKey(key), defaultValue);
- },
- /**
- * @private
- * @param {string} key
- * @param {*} value
- */
- _setItem: function (key, value) {
- setItem(this._generateKey(key), value);
- },
-});
-
-
-//utility functions
-function on(type, listener) {
- if (window.addEventListener) {
- window.addEventListener(type, listener);
- } else { //IE8
- window.attachEvent('on' + type, listener);
- }
-}
-
-function getItem(key, defaultValue) {
- var val = local_storage.getItem(key);
- return val ? JSON.parse(val) : defaultValue;
-}
-
-function setItem(key, value) {
- local_storage.setItem(key, JSON.stringify(value));
-}
-
-
-var tab_manager = {
- isMaster: false,
- id: new Date().getTime() + ':' + (Math.random() * 1000000000 | 0),
-
- register_tab: function (is_master_callback, is_no_longer_master, on_heartbeat_callback, generateKey) {
- this.heartbeatKey = generateKey('heartbeat');
- this.masterKey = generateKey('master');
- this.peersKey = generateKey('peers');
-
- this.is_master_callback = is_master_callback;
- this.is_no_longer_master = is_no_longer_master || function () {};
- this.on_heartbeat_callback = on_heartbeat_callback || function () {};
-
- var peers = getItem(tab_manager.peersKey, {});
- peers[tab_manager.id] = new Date().getTime();
- setItem(tab_manager.peersKey, peers);
-
- on('unload', function () {
- // unload peer
- var peers = getItem(tab_manager.peersKey, {});
- delete peers[tab_manager.id];
- setItem(tab_manager.peersKey, peers);
-
- // unload master
- if (tab_manager.isMaster) {
- local_storage.removeItem(tab_manager.masterKey);
- }
- });
-
- if (!local_storage.getItem(tab_manager.masterKey)) {
- tab_manager.start_election();
- }
-
- on('storage', function (e) {
- if (!e) { e = window.event;}
- if (e.key !== tab_manager.masterKey) {
- return;
- }
-
- if (e.newValue === null) { //master was unloaded
- tab_manager.start_election();
- }
-
- });
- tab_manager.heartbeat();
- },
- heartbeat: function () {
- var current = new Date().getTime();
- var heartbeatValue = local_storage.getItem(tab_manager.heartbeatKey) || 0;
- var peers = getItem(tab_manager.peersKey, {});
-
- if ((parseInt(heartbeatValue) + 5000) < current) {
- // Heartbeat is out of date. Electing new master
- tab_manager.start_election();
- }
- if (tab_manager.isMaster) {
- //walk through all peers and kill old
- var cleanedPeers = {};
- for (var peerName in peers) {
- if (peers[peerName] + 15000 > current) {
- cleanedPeers[peerName] = peers[peerName];
- }
- }
- if (!tab_manager.is_last_heartbeat_mine()) {
- // someone else is also master...
- // it should not happen, except in some race condition situation.
- tab_manager.isMaster = false;
- tab_manager.last_heartbeat = 0;
- peers[tab_manager.id] = current;
- setItem(tab_manager.peersKey, peers);
- tab_manager.is_no_longer_master();
- } else {
- tab_manager.last_heartbeat = current;
- local_storage.setItem(tab_manager.heartbeatKey, current);
- setItem(tab_manager.peersKey, cleanedPeers);
- }
- } else {
- //update own heartbeat
- peers[tab_manager.id] = current;
- setItem(tab_manager.peersKey, peers);
- }
- this.on_heartbeat_callback();
-
- setTimeout(function () {
- tab_manager.heartbeat();
- }, tab_manager.isMaster ? MASTER_TAB_HEARTBEAT_PERIOD : TAB_HEARTBEAT_PERIOD);
- },
- is_last_heartbeat_mine: function () {
- var heartbeatValue = local_storage.getItem(tab_manager.heartbeatKey) || 0;
- return (parseInt(heartbeatValue) === tab_manager.last_heartbeat);
- },
- start_election: function () {
- if (tab_manager.isMaster) {
- return;
- }
- //check who's next
- var peers = getItem(tab_manager.peersKey, {});
- var now = new Date().getTime();
- var newMaster;
-
- for (var peerName in peers) {
- //check for dead peers
- if (peers[peerName] + 15000 < now) {
- continue;
- }
-
- newMaster = peerName;
- break;
- }
- if (newMaster === tab_manager.id) {
- //we're next in queue. Electing as master
- setItem(tab_manager.masterKey, tab_manager.id);
- tab_manager.last_heartbeat = new Date().getTime();
- setItem(tab_manager.heartbeatKey, tab_manager.last_heartbeat);
- tab_manager.isMaster = true;
- tab_manager.is_master_callback();
-
- //removing master peer from queue
- delete peers[newMaster];
- setItem(tab_manager.peersKey, peers);
- }
- },
-};
-
-
-// bus singleton, depending of the browser :
-// if supporting LocalStorage, there will be only one tab polling
-var params = {
- pollRoute: '/longpolling/poll',
- session: session,
-};
-if (typeof Storage !== "undefined") {
- bus.bus = new bus.CrossTabBus(params);
-} else {
- bus.bus = new bus.Bus(params);
-}
-
-return bus;
-
-
-});
-
diff --git a/addons/bus/static/src/js/crosstab_bus.js b/addons/bus/static/src/js/crosstab_bus.js
new file mode 100644
index 00000000000..c0402dcac10
--- /dev/null
+++ b/addons/bus/static/src/js/crosstab_bus.js
@@ -0,0 +1,355 @@
+odoo.define('bus.CrossTab', function (require) {
+"use strict";
+
+var Longpolling = require('bus.Longpolling');
+var session = require('web.session');
+
+
+/**
+ * CrossTab
+ *
+ * This is an extension of the longpolling bus with browser cross-tab synchronization.
+ * It uses a Master/Slaves with Leader Election architecture:
+ * - a single tab handles longpolling.
+ * - tabs are synchronized by means of the local storage.
+ *
+ * localStorage used keys are:
+ * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.channels : shared public channel list to listen during the poll
+ * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.options : shared options
+ * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.notification : the received notifications from the last poll
+ * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.tab_list : list of opened tab ids
+ * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.tab_master : generated id of the master tab
+ *
+ * trigger:
+ * - window_focus : when the window is focused
+ * - notification : when a notification is receive from the long polling
+ * - become_master : when this tab became the master
+ * - no_longer_master : when this tab is not longer the master (the user swith tab)
+ */
+var CrossTabBus = Longpolling.extend({
+ // constants
+ TAB_HEARTBEAT_PERIOD: 10000, // 10 seconds
+ MASTER_TAB_HEARTBEAT_PERIOD: 1500, // 1.5 seconds
+ HEARTBEAT_OUT_OF_DATE_PERIOD: 5000, // 5 seconds
+ HEARTBEAT_KILL_OLD_PERIOD: 15000, // 15 seconds
+ LOCAL_STORAGE_PREFIX: 'bus',
+
+ // properties
+ _isMasterTab: false,
+ _isRegistered: false,
+
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ var now = new Date().getTime();
+ var session = this.getSession();
+ // used to prefix localStorage keys
+ this._sanitizedOrigin = session.origin.replace(/:\/{0,2}/g, '_');
+ // prevents collisions between different tabs and in tests
+ this._id = _.uniqueId(this.LOCAL_STORAGE_PREFIX) + ':' + now;
+ if (this._callLocalStorage('getItem', 'last_ts', 0) + 50000 < now) {
+ this._callLocalStorage('removeItem', 'last');
+ }
+ this._lastNotificationID = this._callLocalStorage('getItem', 'last', -1);
+ this.call('local_storage', 'onStorage', this, this._onStorage);
+ },
+ destroy: function () {
+ this._super();
+ clearTimeout(this._heartbeatTimeout);
+ },
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+ /**
+ * Share the bus channels with the others tab by the local storage
+ *
+ * @override
+ */
+ addChannel: function () {
+ this._super.apply(this, arguments);
+ this._callLocalStorage('setItem', 'channels', this._channels);
+ },
+ /**
+ * Share the bus channels with the others tab by the local storage
+ *
+ * @override
+ */
+ deleteChannel: function () {
+ this._super.apply(this, arguments);
+ this._callLocalStorage('setItem', 'channels', this._channels);
+ },
+ /**
+ * Tells whether this bus is related to the master tab.
+ *
+ * @returns {boolean}
+ */
+ isMasterTab: function () {
+ return this._isMasterTab;
+ },
+ /**
+ * Use the local storage to share the long polling from the master tab.
+ *
+ * @override
+ */
+ startPolling: function () {
+ if (this._isActive === null) {
+ this._heartbeat = this._heartbeat.bind(this);
+ }
+ if (!this._isRegistered) {
+ this._isRegistered = true;
+
+ var peers = this._callLocalStorage('getItem', 'peers', {});
+ peers[this._id] = new Date().getTime();
+ this._callLocalStorage('setItem', 'peers', peers);
+
+ $(window).on('unload.' + this._id, this._onUnload.bind(this));
+
+ if (!this._callLocalStorage('getItem', 'peers')) {
+ this._startElection();
+ }
+
+ this._heartbeat();
+
+ if (this._isMasterTab) {
+ this._callLocalStorage('setItem', 'channels', this._channels);
+ this._callLocalStorage('setItem', 'options', this._options);
+ } else {
+ this._channels = this._callLocalStorage('getItem', 'channels', this._channels);
+ this._options = this._callLocalStorage('getItem', 'options', this._options);
+ }
+ return; // startPolling will be called again on tab registration
+ }
+
+ if (this._isMasterTab) {
+ this._super.apply(this, arguments);
+ }
+ },
+ /**
+ * Share the option with the local storage
+ *
+ * @override
+ */
+ updateOption: function () {
+ this._super.apply(this, arguments);
+ this._callLocalStorage('setItem', 'options', this._options);
+ },
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+ /**
+ * Call local_storage service
+ *
+ * @private
+ * @param {string} method (getItem, setItem, removeItem, on)
+ * @param {string} key
+ * @param {any} param
+ * @returns service information
+ */
+ _callLocalStorage: function (method, key, param) {
+ return this.call('local_storage', method, this._generateKey(key), param);
+ },
+ /**
+ * Generates localStorage keys prefixed by bus. (LOCAL_STORAGE_PREFIX = the name
+ * of this addon), and the sanitized origin, to prevent keys from
+ * conflicting when several bus instances (polling different origins)
+ * co-exist.
+ *
+ * @private
+ * @param {string} key
+ * @returns key prefixed with the origin
+ */
+ _generateKey: function (key) {
+ return this.LOCAL_STORAGE_PREFIX + '.' + this._sanitizedOrigin + '.' + key;
+ },
+ /**
+ * @override
+ * @returns {integer} number of milliseconds since 1 January 1970 00:00:00
+ */
+ _getLastPresence: function () {
+ return this._callLocalStorage('getItem', 'lastPresence') || this._super();
+ },
+ /**
+ * Check all the time (according to the constants) if the tab is the master tab and
+ * check if it is active. Use the local storage for this checks.
+ *
+ * @private
+ * @see _startElection method
+ */
+ _heartbeat: function () {
+ var now = new Date().getTime();
+ var heartbeatValue = parseInt(this._callLocalStorage('getItem', 'heartbeat', 0));
+ var peers = this._callLocalStorage('getItem', 'peers', {});
+
+ if ((heartbeatValue + this.HEARTBEAT_OUT_OF_DATE_PERIOD) < now) {
+ // Heartbeat is out of date. Electing new master
+ this._startElection();
+ heartbeatValue = parseInt(this._callLocalStorage('getItem', 'heartbeat', 0));
+ }
+
+ if (this._isMasterTab) {
+ //walk through all peers and kill old
+ var cleanedPeers = {};
+ for (var peerName in peers) {
+ if (peers[peerName] + this.HEARTBEAT_KILL_OLD_PERIOD > now) {
+ cleanedPeers[peerName] = peers[peerName];
+ }
+ }
+
+ if (heartbeatValue !== this.lastHeartbeat) {
+ // someone else is also master...
+ // it should not happen, except in some race condition situation.
+ this._isMasterTab = false;
+ this.lastHeartbeat = 0;
+ peers[this._id] = now;
+ this._callLocalStorage('setItem', 'peers', peers);
+ this.stopPolling();
+ this.trigger('no_longer_master');
+ } else {
+ this.lastHeartbeat = now;
+ this._callLocalStorage('setItem', 'heartbeat', now);
+ this._callLocalStorage('setItem', 'peers', cleanedPeers);
+ }
+ } else {
+ //update own heartbeat
+ peers[this._id] = now;
+ this._callLocalStorage('setItem', 'peers', peers);
+ }
+
+ // Write lastPresence in local storage if it has been updated since last heartbeat
+ var hbPeriod = this._isMasterTab ? this.MASTER_TAB_HEARTBEAT_PERIOD : this.TAB_HEARTBEAT_PERIOD;
+ if (this._lastPresenceTime + hbPeriod > now) {
+ this._callLocalStorage('setItem', 'lastPresence', this._lastPresenceTime);
+ }
+
+ this._heartbeatTimeout = setTimeout(this._heartbeat, hbPeriod);
+ },
+ /**
+ * Check with the local storage if the current tab is the master tab.
+ * If this tab became the master, trigger 'become_master' event
+ *
+ * @private
+ */
+ _startElection: function () {
+ if (this._isMasterTab) {
+ return;
+ }
+ //check who's next
+ var now = new Date().getTime();
+ var peers = this._callLocalStorage('getItem', 'peers', {});
+ var heartbeatKillOld = now - this.HEARTBEAT_KILL_OLD_PERIOD;
+ var newMaster;
+ for (var peerName in peers) {
+ //check for dead peers
+ if (peers[peerName] < heartbeatKillOld) {
+ continue;
+ }
+ newMaster = peerName;
+ break;
+ }
+
+ if (newMaster === this._id) {
+ //we're next in queue. Electing as master
+ this.lastHeartbeat = now;
+ this._callLocalStorage('setItem', 'heartbeat', this.lastHeartbeat);
+ this._isMasterTab = true;
+ this.startPolling();
+ this.trigger('become_master');
+
+ //removing master peer from queue
+ delete peers[newMaster];
+ this._callLocalStorage('setItem', 'peers', peers);
+ }
+ },
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+ /**
+ * @override
+ */
+ _onFocusChange: function (params) {
+ this._super.apply(this, arguments);
+ this._callLocalStorage('setItem', 'focus', params.focus);
+ },
+ /**
+ * If it's the master tab, the notifications ares broadcasted to other tabs by the
+ * local storage.
+ *
+ * @override
+ */
+ _onPoll: function (notifications) {
+ var notifs = this._super(notifications);
+ if (this._isMasterTab && notifs.length) {
+ this._callLocalStorage('setItem', 'last', this._lastNotificationID);
+ this._callLocalStorage('setItem', 'last_ts', new Date().getTime());
+ this._callLocalStorage('setItem', 'notification', notifs);
+ }
+ },
+ /**
+ * Handler when the local storage is updated
+ *
+ * @private
+ * @param {OdooEvent} event
+ * @param {string} event.key
+ * @param {string} event.newValue
+ */
+ _onStorage: function (e) {
+ var value = JSON.parse(e.newValue);
+ var key = e.key;
+
+ if (this._isRegistered && key === this._generateKey('peers') && !value) {
+ //master was unloaded
+ this._startElection();
+ }
+
+ // last notification id changed
+ if (key === this._generateKey('last')) {
+ this._lastNotificationID = value;
+ }
+ // notifications changed
+
+ if (key === this._generateKey('notification')) {
+ if (!this._isMasterTab) {
+ this.trigger("notification", value);
+ }
+ return;
+ }
+ // update channels
+ if (key === this._generateKey('channels')) {
+ var channels = value;
+ _.each(_.difference(this._channels, channels), this.deleteChannel.bind(this));
+ _.each(_.difference(channels, this._channels), this.addChannel.bind(this));
+ }
+ // update options
+ if (key === this._generateKey('options')) {
+ this._options = value;
+ }
+ // update focus
+ if (key === this._generateKey('focus')) {
+ this._isOdooFocused = value;
+ this.trigger('window_focus', this._isOdooFocused);
+ }
+ },
+ /**
+ * Handler when unload the window
+ *
+ * @private
+ */
+ _onUnload: function () {
+ // unload peer
+ var peers = this._callLocalStorage('getItem', 'peers', {});
+ delete peers[this._id];
+ this._callLocalStorage('setItem', 'peers', peers);
+
+ // unload master
+ if (this._isMasterTab) {
+ this._callLocalStorage('removeItem', 'peers');
+ }
+ },
+});
+
+return CrossTabBus;
+
+});
+
diff --git a/addons/bus/static/src/js/longpolling_bus.js b/addons/bus/static/src/js/longpolling_bus.js
new file mode 100644
index 00000000000..4764e2e52b0
--- /dev/null
+++ b/addons/bus/static/src/js/longpolling_bus.js
@@ -0,0 +1,255 @@
+odoo.define('bus.Longpolling', function (require) {
+"use strict";
+
+var Bus = require('web.Bus');
+var ServicesMixin = require('web.ServicesMixin');
+
+
+/**
+ * Event Longpolling bus used to bind events on the server long polling return
+ *
+ * trigger:
+ * - window_focus : when the window focus change (true for focused, false for blur)
+ * - notification : when a notification is receive from the long polling
+ *
+ * @class Longpolling
+ */
+var LongpollingBus = Bus.extend(ServicesMixin, {
+ // constants
+ PARTNERS_PRESENCE_CHECK_PERIOD: 30000, // don't check presence more than once every 30s
+ ERROR_RETRY_DELAY: 10000, // 10 seconds
+ POLL_ROUTE: '/longpolling/poll',
+
+ // properties
+ _isActive: null,
+ _lastNotificationID: 0,
+ _isOdooFocused: true,
+ _pollRetryTimeout: null,
+
+ /**
+ * @override
+ */
+ init: function (parent, params) {
+ this._super.apply(this, arguments);
+ this._id = _.uniqueId('bus');
+ this._options = {};
+ this._channels = [];
+
+ // bus presence
+ this._lastPresenceTime = new Date().getTime();
+ this._lastPartnersPresenceCheck = this._lastPresenceTime;
+ $(window).on("focus." + this._id, this._onFocusChange.bind(this, {focus: true}));
+ $(window).on("blur." + this._id, this._onFocusChange.bind(this, {focus: false}));
+ $(window).on("unload." + this._id, this._onFocusChange.bind(this, {focus: false}));
+
+ $(window).on("click." + this._id, this._onPresence.bind(this));
+ $(window).on("keydown." + this._id, this._onPresence.bind(this));
+ $(window).on("keyup." + this._id, this._onPresence.bind(this));
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ this.stopPolling();
+ $(window).off("focus." + this._id);
+ $(window).off("blur." + this._id);
+ $(window).off("unload." + this._id);
+ $(window).off("click." + this._id);
+ $(window).off("keydown." + this._id);
+ $(window).off("keyup." + this._id);
+ },
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+ /**
+ * Register a new channel to listen on the longpoll (ignore if already
+ * listening on this channel).
+ * Aborts a pending longpoll, in order to re-start another longpoll, so
+ * that we can immediately get notifications on newly registered channel.
+ *
+ * @param {string} channel
+ */
+ addChannel: function (channel) {
+ if (this._channels.indexOf(channel) === -1) {
+ this._channels.push(channel);
+ if (this._pollRpc) {
+ this._pollRpc.abort();
+ } else {
+ this.startPolling();
+ }
+ }
+ },
+ /**
+ * Unregister a channel from listening on the longpoll.
+ *
+ * Aborts a pending longpoll, in order to re-start another longpoll, so
+ * that we immediately remove ourselves from listening on notifications
+ * on this channel.
+ *
+ * @param {string} channel
+ */
+ deleteChannel: function (channel) {
+ var index = this._channels.indexOf(channel);
+ if (index !== -1) {
+ this._channels.splice(index, 1);
+ if (this._pollRpc) {
+ this._pollRpc.abort();
+ }
+ }
+ },
+ /**
+ * Tell whether odoo is focused or not
+ *
+ * @returns {boolean}
+ */
+ isOdooFocused: function () {
+ return this._isOdooFocused;
+ },
+ /**
+ * Start a long polling, i.e. it continually opens a long poll
+ * connection as long as it is not stopped (@see `stopPolling`)
+ */
+ startPolling: function () {
+ if (this._isActive === null) {
+ this._poll = this._poll.bind(this);
+ }
+ if (!this._isActive) {
+ this._isActive = true;
+ this._poll();
+ }
+ },
+ /**
+ * Stops any started long polling
+ *
+ * Aborts a pending longpoll so that we immediately remove ourselves
+ * from listening on notifications on this channel.
+ */
+ stopPolling: function () {
+ this._isActive = false;
+ this._channels = [];
+ clearTimeout(this._pollRetryTimeout);
+ if (this._pollRpc) {
+ this._pollRpc.abort();
+ }
+ },
+ /**
+ * Add or update an option on the longpoll bus.
+ * Stored options are sent to the server whenever a poll is started.
+ *
+ * @param {string} key
+ * @param {any} value
+ */
+ updateOption: function (key, value) {
+ this._options[key] = value;
+ },
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+ /**
+ * returns the last recorded presence
+ *
+ * @private
+ * @returns {integer} number of milliseconds since 1 January 1970 00:00:00
+ */
+ _getLastPresence: function () {
+ return this._lastPresenceTime;
+ },
+ /**
+ * Continually start a poll:
+ *
+ * A poll is a connection that is kept open for a relatively long period
+ * (up to 1 minute). Local bus data are sent to the server each time a poll
+ * is initiated, and the server may return some "real-time" notifications
+ * about registered channels.
+ *
+ * A poll ends on timeout, on abort, on receiving some notifications, or on
+ * receiving an error. Another poll usually starts afterward, except if the
+ * poll is aborted or stopped (@see stopPolling).
+ *
+ * @private
+ */
+ _poll: function () {
+ var self = this;
+ if (!this._isActive) {
+ return;
+ }
+ var now = new Date().getTime();
+ var options = _.extend({}, this._options, {
+ bus_inactivity: now - this._getLastPresence(),
+ });
+ if (this._lastPartnersPresenceCheck + this.PARTNERS_PRESENCE_CHECK_PERIOD > now) {
+ options = _.omit(options, 'bus_presence_partner_ids');
+ } else {
+ this._lastPartnersPresenceCheck = now;
+ }
+ var data = {channels: this._channels, last: this._lastNotificationID, options: options};
+ // The backend has a maximum cycle time of 50 seconds so give +10 seconds
+ this._pollRpc = this._rpc({route: this.POLL_ROUTE, params: data}, {shadow : true, timeout: 60000});
+ this._pollRpc.then(function (result) {
+ self._pollRpc = false;
+ self._onPoll(result);
+ self._poll();
+ }, function (error, ev) {
+ self._pollRpc = false;
+ // no error popup if request is interrupted or fails for any reason
+ ev.preventDefault();
+ if (error && error.message === "XmlHttpRequestError abort") {
+ self._poll();
+ } else {
+ // random delay to avoid massive longpolling
+ self._pollRetryTimeout = setTimeout(self._poll, self.ERROR_RETRY_DELAY + (Math.floor((Math.random()*20)+1)*1000));
+ }
+ });
+ },
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+ /**
+ * Handler when the focus of the window change.
+ * Trigger the 'window_focus' event.
+ *
+ * @private
+ * @param {Object} params
+ * @param {Boolean} params.focus
+ */
+ _onFocusChange: function (params) {
+ this._isOdooFocused = params.focus;
+ if (params.focus) {
+ this._lastPresenceTime = new Date().getTime();
+ this.trigger('window_focus', this._isOdooFocused);
+ }
+ },
+ /**
+ * Handler when the long polling receive the new notifications
+ * Update the last notification id received.
+ * Triggered the 'notification' event with a list [channel, message] from notifications.
+ *
+ * @private
+ * @param {Object[]} notifications, Input notifications have an id, channel, message
+ * @returns {Array[]} Output arrays have notification's channel and message
+ */
+ _onPoll: function (notifications) {
+ var self = this;
+ var notifs = _.map(notifications, function (notif) {
+ if (notif.id > self._lastNotificationID) {
+ self._lastNotificationID = notif.id;
+ }
+ return [notif.channel, notif.message];
+ });
+ this.trigger("notification", notifs);
+ return notifs;
+ },
+ /**
+ * Handler when they are an activity on the window (click, keydown, keyup)
+ * Update the last presence date.
+ *
+ * @private
+ */
+ _onPresence: function () {
+ this._lastPresenceTime = new Date().getTime();
+ },
+});
+
+return LongpollingBus;
+
+});
diff --git a/addons/bus/static/src/js/services/bus_service.js b/addons/bus/static/src/js/services/bus_service.js
index e0bf8b42636..8e367b7f93a 100644
--- a/addons/bus/static/src/js/services/bus_service.js
+++ b/addons/bus/static/src/js/services/bus_service.js
@@ -1,34 +1,27 @@
odoo.define('bus.BusService', function (require) {
"use strict";
-var bus = require('bus.bus').bus;
-
-var AbstractService = require('web.AbstractService');
+var CrossTab = require('bus.CrossTab');
var core = require('web.core');
+var ServicesMixin = require('web.ServicesMixin');
+
+var BusService = CrossTab.extend(ServicesMixin, {
+ dependencies : ['local_storage'],
+
+ // properties
+ _audio: null,
-var BusService = AbstractService.extend({
/**
- * @override
+ * This method is necessary in order for this Class to be used to instantiate services
+ *
+ * @abstract
*/
- start: function () {
- this._super.apply(this, arguments);
- this.bus = bus;
- this._audio = null;
- },
+ start: function () {},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
- /**
- * Get the bus
- *
- * @return {web.Bus} the longpoll bus
- */
- getBus: function () {
- return this.bus;
- },
-
/**
* Send a notification, and notify once per browser's tab
*
@@ -37,16 +30,25 @@ var BusService = AbstractService.extend({
*/
sendNotification: function (title, content) {
if (window.Notification && Notification.permission === "granted") {
- if (this.bus.is_master) {
+ if (this.isMasterTab()) {
this._sendNativeNotification(title, content);
}
} else {
this.do_notify(title, content);
- if (this.bus.is_master) {
+ if (this.isMasterTab()) {
this._beep();
}
}
},
+ /**
+ * Register listeners on notifications received on this bus service
+ *
+ * @param {Object} receiver
+ * @param {function} func
+ */
+ onNotification: function () {
+ this.on.apply(this, ["notification"].concat(Array.prototype.slice.call(arguments)));
+ },
//--------------------------------------------------------------------------
// Private
diff --git a/addons/bus/static/tests/bus_tests.js b/addons/bus/static/tests/bus_tests.js
new file mode 100644
index 00000000000..33255f8d8de
--- /dev/null
+++ b/addons/bus/static/tests/bus_tests.js
@@ -0,0 +1,163 @@
+odoo.define('web.bus_tests', function (require) {
+"use strict";
+
+var BusService = require('bus.BusService');
+var AbstractStorageService = require('web.AbstractStorageService');
+var RamStorage = require('web.RamStorage');
+var testUtils = require('web.test_utils');
+var Widget = require('web.Widget');
+
+
+var LocalStorageServiceMock;
+
+BusService = BusService.extend({
+ TAB_HEARTBEAT_PERIOD: 10,
+ MASTER_TAB_HEARTBEAT_PERIOD: 1,
+});
+
+
+QUnit.module('Bus', {
+ beforeEach: function () {
+ LocalStorageServiceMock = AbstractStorageService.extend({storage: new RamStorage()});
+ },
+}, function () {
+ QUnit.test('notifications received from the longpolling channel', function (assert) {
+ assert.expect(6);
+
+ var pollDeferred = $.Deferred();
+
+ var parent = new Widget();
+ testUtils.addMockEnvironment(parent, {
+ data: {},
+ services: {
+ bus_service: BusService,
+ local_storage: LocalStorageServiceMock,
+ },
+ mockRPC: function (route, args) {
+ if (route === '/longpolling/poll') {
+ assert.step([route, args.channels.join(',')]);
+
+ pollDeferred = $.Deferred();
+ pollDeferred.abort = (function () {
+ this.reject({message: "XmlHttpRequestError abort"}, $.Event());
+ }).bind(pollDeferred);
+ return pollDeferred;
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ var widget = new Widget(parent);
+ widget.appendTo($('#qunit-fixture'));
+
+ widget.call('bus_service', 'onNotification', this, function (notifications) {
+ assert.step(['notification', notifications]);
+ });
+ widget.call('bus_service', 'addChannel', 'lambda');
+
+ pollDeferred.resolve([{
+ id: 1,
+ channel: 'lambda',
+ message: 'beta',
+ }]);
+ pollDeferred.resolve([{
+ id: 2,
+ channel: 'lambda',
+ message: 'epsilon',
+ }]);
+
+ assert.verifySteps([
+ ["/longpolling/poll", "lambda"],
+ ["notification", [["lambda","beta"]]],
+ ["/longpolling/poll", "lambda"],
+ ["notification", [["lambda","epsilon"]]],
+ ["/longpolling/poll", "lambda"]
+ ]);
+
+ parent.destroy();
+ });
+
+ QUnit.test('cross tab bus share message from a channel', function (assert) {
+ var done = assert.async();
+ assert.expect(5);
+
+ // master
+
+ var pollDeferredMaster = $.Deferred();
+
+ var parentMaster = new Widget();
+ testUtils.addMockEnvironment(parentMaster, {
+ data: {},
+ services: {
+ bus_service: BusService,
+ local_storage: LocalStorageServiceMock,
+ },
+ mockRPC: function (route, args) {
+ if (route === '/longpolling/poll') {
+ assert.step(['master', route, args.channels.join(',')]);
+
+ pollDeferredMaster = $.Deferred();
+ pollDeferredMaster.abort = (function () {
+ this.reject({message: "XmlHttpRequestError abort"}, $.Event());
+ }).bind(pollDeferredMaster);
+ return pollDeferredMaster;
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ var master = new Widget(parentMaster);
+ master.appendTo($('#qunit-fixture'));
+
+ master.call('bus_service', 'onNotification', master, function (notifications) {
+ assert.step(['master', 'notification', notifications]);
+ });
+ master.call('bus_service', 'addChannel', 'lambda');
+
+ // slave
+
+ setTimeout(function () {
+ var parentSlave = new Widget();
+ testUtils.addMockEnvironment(parentSlave, {
+ data: {},
+ services: {
+ bus_service: BusService,
+ local_storage: LocalStorageServiceMock,
+ },
+ mockRPC: function (route, args) {
+ if (route === '/longpolling/poll') {
+ throw new Error("Can not use the longpolling of the slave client");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ var slave = new Widget(parentSlave);
+ slave.appendTo($('#qunit-fixture'));
+
+ slave.call('bus_service', 'onNotification', slave, function (notifications) {
+ assert.step(['slave', 'notification', notifications]);
+ });
+ slave.call('bus_service', 'addChannel', 'lambda');
+
+ pollDeferredMaster.resolve([{
+ id: 1,
+ channel: 'lambda',
+ message: 'beta',
+ }]);
+
+ assert.verifySteps([
+ ["master", "/longpolling/poll", "lambda"],
+ ["master", "notification", [["lambda", "beta"]]],
+ ["slave", "notification", [["lambda", "beta"]]],
+ ["master", "/longpolling/poll", "lambda"],
+ ]);
+
+ parentMaster.destroy();
+ parentSlave.destroy();
+
+ done();
+
+ }, 3);
+ });
+});});
diff --git a/addons/bus/views/bus.xml b/addons/bus/views/bus.xml
index 914393eefb6..4d8f7bc36de 100644
--- a/addons/bus/views/bus.xml
+++ b/addons/bus/views/bus.xml
@@ -1,11 +1,16 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js
index e09222b6025..897c8ef09e2 100644
--- a/addons/calendar/static/src/js/base_calendar.js
+++ b/addons/calendar/static/src/js/base_calendar.js
@@ -100,7 +100,7 @@ WebClient.include({
// in which the current user is involved is created, edited or deleted
this.calendar_notif_timeouts = {};
this.calendar_notif = {};
- this.call('bus_service', 'getBus').on('notification', this, function (notifications) {
+ this.call('bus_service', 'onNotification', this, function (notifications) {
_.each(notifications, (function (notification) {
if (notification[0][1] === 'calendar.alarm') {
this.display_calendar_notif(notification[1]);
diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js
index 26ea7dbfb8e..3e3555516c5 100644
--- a/addons/im_livechat/static/src/js/im_livechat.js
+++ b/addons/im_livechat/static/src/js/im_livechat.js
@@ -1,7 +1,7 @@
odoo.define('im_livechat.im_livechat', function (require) {
"use strict";
-var bus = require('bus.bus').bus;
+require('bus.BusService');
var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
@@ -63,7 +63,6 @@ var LivechatButton = Widget.extend({
this._chatWindow = null;
this._messages = [];
this._serverURL = serverURL;
- this._busBus = bus;
},
willStart: function () {
var self = this;
@@ -98,7 +97,7 @@ var LivechatButton = Widget.extend({
setTimeout(this._openChat.bind(this), this._rule.auto_popup_timer*1000);
}
}
- this._busBus.on('notification', this, this._onNotification);
+ this.call('bus_service', 'onNotification', this, this._onNotification);
return this._super();
},
@@ -210,8 +209,8 @@ var LivechatButton = Widget.extend({
self._sendWelcomeMessage();
self._renderMessages();
- self._busBus.add_channel(self._livechat.getUUID());
- self._busBus.start_polling();
+ self.busBus.addChannel(self._livechat.getUUID());
+ self.busBus.startPolling();
utils.set_cookie('im_livechat_session', JSON.stringify(self._livechat.toData()), 60*60);
utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml
index b12cba1b621..bc44e4c79fb 100644
--- a/addons/im_livechat/views/im_livechat_channel_templates.xml
+++ b/addons/im_livechat/views/im_livechat_channel_templates.xml
@@ -138,8 +138,12 @@
-
-
+
+
+
+
+
+
@@ -175,9 +179,10 @@
odoo.define('im_livechat.livesupport', function (require) {
+ var websiteRoot = require('root.widget');
var im_livechat = require('im_livechat.im_livechat');
var button = new im_livechat.LivechatButton(
- $('body'),
+ websiteRoot,
"",
);
diff --git a/addons/im_support/static/src/js/mail_manager.js b/addons/im_support/static/src/js/mail_manager.js
index 56ea4b736b6..193f78d692b 100644
--- a/addons/im_support/static/src/js/mail_manager.js
+++ b/addons/im_support/static/src/js/mail_manager.js
@@ -10,7 +10,6 @@ var WebClient = require('web.WebClient');
var SupportChannel = require('im_support.SupportChannel');
var SupportMessage = require('im_support.SupportMessage');
var supportSession = require('im_support.SupportSession');
-var supportBus = require('im_support.SupportBus');
var _t = core._t;
@@ -24,6 +23,7 @@ var SUPPORT_CHANNEL_STATE_KEY = 'im_support.channel_state';
* livechat operators from another database (the Support database).
*/
MailManager.include({
+ dependencies: (MailManager.prototype.dependencies || []).concat(['support_bus_service']),
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -43,7 +43,7 @@ MailManager.include({
this.pollingSupport = false;
// listen to notifications coming from Support longpolling
- supportBus.on('notification', this, this._onSupportNotification);
+ this.call('support_bus_service', 'onNotification', this, this._onSupportNotification);
// check if there is a pending chat session with the Support
var timeoutTimestamp = this.call('local_storage', 'getItem', POLL_TIMEOUT_KEY);
@@ -79,10 +79,13 @@ MailManager.include({
* timeout delay to set
*/
startPollingSupport: function (pollingDelay) {
+ if (!('pollingSupport' in this)) {
+ return this.initSupport();
+ }
if (!this.pollingSupport) {
this.pollingSupport = true;
- supportBus.add_channel(this.supportChannelUUID);
- supportBus.start_polling();
+ this.call('support_bus_service', 'addChannel', this.supportChannelUUID);
+ this.call('support_bus_service', 'startPolling');
this._setPollTimeout(pollingDelay);
}
},
@@ -212,7 +215,7 @@ MailManager.include({
*/
_stopPollingSupport: function () {
this.pollingSupport = false;
- supportBus.stop_polling();
+ this.call('support_bus_service', 'stopPolling');
this.call('local_storage', 'removeItem', POLL_TIMEOUT_KEY);
},
diff --git a/addons/im_support/static/src/js/support_bus.js b/addons/im_support/static/src/js/support_bus.js
index 70c4e1dd3c1..bb7c3386780 100644
--- a/addons/im_support/static/src/js/support_bus.js
+++ b/addons/im_support/static/src/js/support_bus.js
@@ -6,23 +6,21 @@ odoo.define('im_support.SupportBus', function (require) {
* to poll the Support server.
*/
+var BusService = require('bus.BusService');
var supportSession = require('im_support.SupportSession');
+var core = require('web.core');
-var bus = require('bus.bus');
+var SupportBusService = BusService.extend({
+ LOCAL_STORAGE_PREFIX: 'im_support',
+ POLL_ROUTE: '/longpolling/support_poll',
+ getSession: function () {
+ return supportSession;
+ },
+});
-var Bus;
-if(typeof Storage !== "undefined"){
- Bus = bus.CrossTabBus;
-} else {
- Bus = bus.Bus;
-}
+core.serviceRegistry.add('support_bus_service', SupportBusService);
-var params = {
- session: supportSession,
- pollRoute: '/longpolling/support_poll',
-};
-
-return new Bus(params);
+return SupportBusService;
});
diff --git a/addons/im_support/static/tests/helpers/test_utils.js b/addons/im_support/static/tests/helpers/test_utils.js
index 9ded775eed0..3347c43029f 100644
--- a/addons/im_support/static/tests/helpers/test_utils.js
+++ b/addons/im_support/static/tests/helpers/test_utils.js
@@ -1,11 +1,20 @@
odoo.define('im_support.test_utils', function (require) {
"use strict";
+var mailTestUtils = require('mail.testUtils');
var supportSession = require('im_support.SupportSession');
var testUtils = require('web.test_utils');
+mailTestUtils.MockMailService.include({
+ getServices: function () {
+ return _.extend(this._super(), {
+ support_bus_service: this.bus_service(),
+ });
+ },
+});
+
/**
* Extended version of addMockEnvironment that mocks RPCs done to the Support
* server (CORS), and enables a longpolling to the Support server, optionally.
diff --git a/addons/im_support/static/tests/systray_tests.js b/addons/im_support/static/tests/systray_tests.js
index 848cf669b92..09a8f600642 100644
--- a/addons/im_support/static/tests/systray_tests.js
+++ b/addons/im_support/static/tests/systray_tests.js
@@ -1,7 +1,6 @@
odoo.define('im_support.systray_tests', function (require) {
"use strict";
-var supportBus = require('im_support.SupportBus');
var imSupportTestUtils = require('im_support.test_utils');
var mailTestUtils = require('mail.testUtils');
@@ -21,6 +20,7 @@ QUnit.module('systray', {
},
};
this.services = mailTestUtils.getMailServices();
+
this.supportParams = {
db_uuid: 'some_uuid',
support_token: 'ABCDEFGHIJ',
@@ -316,7 +316,7 @@ QUnit.test('receive messages in the Support channel', function (assert) {
channel_ids: [supportChannelID],
};
var notification = [[false, 'mail.channel'], data];
- supportBus.trigger('notification', [notification]);
+ messagingMenu.call('support_bus_service', 'trigger', 'notification', [notification]);
assert.strictEqual($('.o_thread_window .o_thread_message').length, 1,
"there should be a new message in the thread");
diff --git a/addons/im_support/views/assets.xml b/addons/im_support/views/assets.xml
index 34cb5f2482f..60bbd7be798 100644
--- a/addons/im_support/views/assets.xml
+++ b/addons/im_support/views/assets.xml
@@ -17,10 +17,17 @@
+
+
+
+
+
+
+
@@ -35,7 +42,6 @@
-
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 b1417d62fc6..6e4c8fb591c 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
@@ -25,9 +25,9 @@ MailManager.include({
// 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, '{}');
+ this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, {});
} else {
- this.isReady().then(this._updateDocumentThreadWindows.bind(this, JSON.parse(state)));
+ this.isReady().then(this._updateDocumentThreadWindows.bind(this, state));
}
// listen to localStorage changes to synchronize DocumentThread's
// windows between tabs
@@ -67,7 +67,7 @@ MailManager.include({
data.author_id[0] === session.partner_id
) {
var key = this.DOCUMENT_THREAD_MESSAGE_KEY;
- this.call('local_storage', 'setItem', key, JSON.stringify(data));
+ this.call('local_storage', 'setItem', key, data);
}
return this._super.apply(this, arguments);
},
@@ -149,15 +149,12 @@ MailManager.include({
* @param {string} state.windowState ('closed', 'folded' or 'open')
*/
updateDocumentThreadState: function (threadID, state) {
- var item = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
- var states = JSON.parse(item);
+ var states = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
states = _.omit(states, function (state) {
return state.windowState === 'closed';
});
states[threadID] = state;
- this.call('local_storage', 'setItem',
- this.DOCUMENT_THREAD_STATE_KEY,
- JSON.stringify(states));
+ this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, states);
},
//--------------------------------------------------------------------------
@@ -223,11 +220,11 @@ MailManager.include({
_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(JSON.parse(state));
+ this._updateDocumentThreadWindows(state);
} 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(JSON.parse(message));
+ this.addMessage(message);
}
}
},
diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js
index d04028267c2..a5eae053a82 100644
--- a/addons/mail/static/src/js/services/mail_manager.js
+++ b/addons/mail/static/src/js/services/mail_manager.js
@@ -51,7 +51,6 @@ var MailManager = AbstractService.extend({
* @override
*/
start: function () {
- this._busBus = this.call('bus_service', 'getBus');
this._cannedResponses = [];
this._mailBus = new Bus(this);
this._commands = [];
@@ -470,7 +469,7 @@ var MailManager = AbstractService.extend({
channel = this._makeChannel(data, options);
if (channel.getType() === 'dm') {
this._pinnedDmPartners.push(channel.getDirectPartnerID());
- this._busBus.update_option(
+ this.call('bus_service', 'updateOption',
'bus_presence_partner_ids',
this._pinnedDmPartners
);
@@ -862,7 +861,7 @@ var MailManager = AbstractService.extend({
});
}).then(function (result) {
self._updateFromServer(result);
- self._busBus.start_polling();
+ self.call('bus_service', 'startPolling');
});
},
/**
@@ -1001,7 +1000,7 @@ var MailManager = AbstractService.extend({
* @param {boolean} options.isDisplayed
*/
_notifyIncomingMessage: function (message, options) {
- if (this._busBus.is_odoo_focused() && options.isDisplayed) {
+ if (this.call('bus_service', 'isOdooFocused') && options.isDisplayed) {
// no need to notify
return;
}
@@ -1012,7 +1011,7 @@ var MailManager = AbstractService.extend({
var content = mailUtils.parseAndTransform(message.getBody(), mailUtils.stripHTML)
.substr(0, PREVIEW_MSG_MAX_SIZE);
- if (!this._busBus.is_odoo_focused()) {
+ if (!this.call('bus_service', 'isOdooFocused')) {
this._outOfFocusUnreadMessageCounter++;
var tabTitle = _.str.sprintf(
_t("%d Messages"),
@@ -1112,7 +1111,7 @@ var MailManager = AbstractService.extend({
var index = this._pinnedDmPartners.indexOf(channel.getDirectPartnerID());
if (index > -1) {
this._pinnedDmPartners.splice(index, 1);
- this._busBus.update_option(
+ this.call('bus_service', 'updateOption',
'bus_presence_partner_ids',
this._pinnedDmPartners
);
diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js
index cd0ff9223a0..b3e5239f9ee 100644
--- a/addons/mail/static/src/js/services/mail_notification_manager.js
+++ b/addons/mail/static/src/js/services/mail_notification_manager.js
@@ -18,7 +18,7 @@ MailManager.include({
start: function () {
this._super.apply(this, arguments);
- this._busBus.on('notification', this, this._onNotification);
+ this.call('bus_service', 'onNotification', this, this._onNotification);
},
//--------------------------------------------------------------------------
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index 17f304b067a..d07f000b129 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -424,8 +424,6 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
var done = assert.async();
assert.expect(27);
- var bus = this.services.bus_service.prototype.bus;
-
this.data.partner.records[0].message_ids = [1];
this.data['mail.message'].records = [{
author_id: ["1", "John Doe"],
@@ -499,7 +497,7 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
type: 'toggle_star',
};
var notification = [[false, 'res.partner'], data];
- bus.trigger('notification', [notification]);
+ form.call('bus_service', 'trigger', 'notification', [notification]);
return $.when();
}
return this._super(route, args);
@@ -784,7 +782,6 @@ QUnit.test('chatter: discard changes on message post with post_refresh "recipien
var getSuggestionsDef = $.Deferred();
var messages = [];
- var bus = this.services.bus_service.prototype.bus;
var form = createView({
View: FormView,
model: 'partner',
@@ -1664,7 +1661,7 @@ QUnit.test('does not render and crash when destroyed before chat system is ready
intercepts: {
get_session: function (event) {
event.stopPropagation();
- event.data.callback({uid: 1});
+ event.data.callback({uid: 1, origin: 'http://web'});
},
},
});
diff --git a/addons/mail/static/tests/discuss_moderation_tests.js b/addons/mail/static/tests/discuss_moderation_tests.js
index 65efc32cc36..e324decba8c 100644
--- a/addons/mail/static/tests/discuss_moderation_tests.js
+++ b/addons/mail/static/tests/discuss_moderation_tests.js
@@ -533,8 +533,6 @@ QUnit.test('author: send message in moderated channel', function (assert) {
assert.expect(4);
var done = assert.async();
- var bus = this.services.bus_service.prototype.bus;
-
var messagePostDef = $.Deferred();
this.data.initMessaging = {
@@ -548,6 +546,7 @@ QUnit.test('author: send message in moderated channel', function (assert) {
},
};
+ var objectDiscuss;
createDiscuss({
id: 1,
context: {},
@@ -571,7 +570,7 @@ QUnit.test('author: send message in moderated channel', function (assert) {
message: message,
};
var notification = [metaData, notifData];
- bus.trigger('notification', [notification]);
+ objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
messagePostDef.resolve();
return $.when(message.id);
@@ -583,6 +582,7 @@ QUnit.test('author: send message in moderated channel', function (assert) {
},
})
.then(function (discuss) {
+ objectDiscuss = discuss;
// go to channel 'general'
discuss.$('.o_mail_discuss_item[data-thread-id="1"]').click();
@@ -613,8 +613,6 @@ QUnit.test('author: sent message accepted in moderated channel', function (asser
assert.expect(8);
var done = assert.async();
- var bus = this.services.bus_service.prototype.bus;
-
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
@@ -674,7 +672,7 @@ QUnit.test('author: sent message accepted in moderated channel', function (asser
};
var metaData = [dbName, 'mail.channel'];
var notification = [metaData, messageData];
- bus.trigger('notification', [notification]);
+ discuss.call('bus_service', 'trigger', 'notification', [notification]);
// check message is accepted
$message = discuss.$('.o_thread_message');
@@ -696,8 +694,6 @@ QUnit.test('author: sent message rejected in moderated channel', function (asser
assert.expect(5);
var done = assert.async();
- var bus = this.services.bus_service.prototype.bus;
-
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
@@ -753,7 +749,7 @@ QUnit.test('author: sent message rejected in moderated channel', function (asser
};
var metaData = [dbName, 'res.partner'];
var notification = [metaData, notifData];
- bus.trigger('notification', [notification]);
+ discuss.call('bus_service', 'trigger', 'notification', [notification]);
// // check no message
assert.strictEqual(discuss.$('.o_thread_message').length, 0,
diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index 9d61a0d2446..57a8c00ab26 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -146,7 +146,6 @@ QUnit.test('@ mention in channel', function (assert) {
assert.expect(34);
var done = assert.async();
- var bus = this.services.bus_service.prototype.bus;
var fetchListenersDef = $.Deferred();
var receiveMessageDef = $.Deferred();
@@ -160,6 +159,7 @@ QUnit.test('@ mention in channel', function (assert) {
},
};
+ var objectDiscuss;
createDiscuss({
id: 1,
context: {},
@@ -182,7 +182,7 @@ QUnit.test('@ mention in channel', function (assert) {
channel_ids: [1],
};
var notification = [[false, 'mail.channel'], data];
- bus.trigger('notification', [notification]);
+ objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
receiveMessageDef.resolve();
return $.when(42);
}
@@ -190,6 +190,8 @@ QUnit.test('@ mention in channel', function (assert) {
},
})
.then(function (discuss) {
+ objectDiscuss = discuss;
+
var $general = discuss.$('.o_mail_discuss_sidebar')
.find('.o_mail_discuss_item[data-thread-id=1]');
assert.strictEqual($general.length, 1,
@@ -432,7 +434,6 @@ QUnit.test('"Unstar all" button should reset the starred counter', function (ass
assert.expect(2);
var done = assert.async();
- var bus = this.services.bus_service.prototype.bus;
var messageData = [];
_.each(_.range(1, 41), function (num) {
messageData.push({
@@ -458,6 +459,7 @@ QUnit.test('"Unstar all" button should reset the starred counter', function (ass
};
this.data['mail.message'].records = messageData;
+ var objectDiscuss;
createDiscuss({
id: 1,
context: {},
@@ -472,7 +474,7 @@ QUnit.test('"Unstar all" button should reset the starred counter', function (ass
type: 'toggle_star',
};
var notification = [[false, 'res.partner'], data];
- bus.trigger('notification', [notification]);
+ objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
return $.when(42);
}
return this._super.apply(this, arguments);
@@ -480,6 +482,8 @@ QUnit.test('"Unstar all" button should reset the starred counter', function (ass
session: {partner_id: 1},
})
.then(function (discuss) {
+ objectDiscuss = discuss;
+
var $starred = discuss.$('.o_mail_discuss_sidebar').find('.o_mail_mailbox_title_starred');
var $starredCounter = $('.o_mail_mailbox_title_starred > .o_mail_sidebar_needaction');
@@ -624,7 +628,6 @@ QUnit.test('convert emoji sources to unicodes on message_post', function (assert
assert.expect(2);
var done = assert.async();
- var bus = this.services.bus_service.prototype.bus;
var receiveMessageDef = $.Deferred();
this.data.initMessaging = {
@@ -637,6 +640,7 @@ QUnit.test('convert emoji sources to unicodes on message_post', function (assert
},
};
+ var objectDiscuss;
createDiscuss({
id: 1,
context: {},
@@ -654,7 +658,7 @@ QUnit.test('convert emoji sources to unicodes on message_post', function (assert
channel_ids: [1],
};
var notification = [[false, 'mail.channel'], data];
- bus.trigger('notification', [notification]);
+ objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
receiveMessageDef.resolve();
return $.when(42);
}
@@ -662,6 +666,8 @@ QUnit.test('convert emoji sources to unicodes on message_post', function (assert
},
})
.then(function (discuss) {
+ objectDiscuss= discuss;
+
var $general = discuss.$('.o_mail_discuss_sidebar')
.find('.o_mail_discuss_item[data-thread-id=1]');
diff --git a/addons/mail/static/tests/document_thread_window_tests.js b/addons/mail/static/tests/document_thread_window_tests.js
index c9814ada195..8cae9862dea 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.strictEqual(ev.data.args[1], JSON.stringify(newMessage),
+ assert.deepEqual(ev.data.args[1], newMessage,
"should write sent message in local storage, to share info with other tabs");
}
}, true);
@@ -262,10 +262,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/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js
index ae0f18293ff..a6b0b67a037 100644
--- a/addons/mail/static/tests/helpers/test_utils.js
+++ b/addons/mail/static/tests/helpers/test_utils.js
@@ -1,12 +1,13 @@
odoo.define('mail.testUtils', function (require) {
"use strict";
+var BusService = require('bus.BusService');
+
var Discuss = require('mail.Discuss');
var MailService = require('mail.Service');
var AbstractService = require('web.AbstractService');
var AbstractStorageService = require('web.AbstractStorageService');
-var Bus = require('web.Bus');
var Class = require('web.Class');
var ControlPanel = require('web.ControlPanel');
var RamStorage = require('web.RamStorage');
@@ -63,27 +64,10 @@ function createDiscuss(params) {
var MockMailService = Class.extend({
bus_service: function () {
- var MockBus = Bus.extend({
- /**
- * Do nothing
- */
- startPolling: function () {},
+ return BusService.extend({
+ _poll: function () {}, // Do nothing
isOdooFocused: function () { return true; },
});
- return AbstractService.extend({
- bus: new MockBus(this),
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * @returns {Bus}
- */
- getBus: function () {
- return this.bus;
- }
- });
},
mail_service: function () {
return MailService;
diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
index 347233efd31..634eef57d18 100644
--- a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
+++ b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js
@@ -149,8 +149,6 @@ QUnit.test('messaging menu widget: messaging menu with 1 record', function (asse
QUnit.test('messaging menu widget: no crash when clicking on inbox notification not associated to a document', function (assert) {
assert.expect(3);
- var bus = this.services.bus_service.prototype.bus;
-
var messagingMenu = new MessagingMenu();
testUtils.addMockEnvironment(messagingMenu, {
services: this.services,
@@ -185,7 +183,7 @@ QUnit.test('messaging menu widget: no crash when clicking on inbox notification
var notifications = [
[['myDB', 'ir.needaction'], message]
];
- bus.trigger('notification', notifications);
+ messagingMenu.call('bus_service', 'trigger', 'notification', notifications);
// Open messaging menu
messagingMenu.$('.dropdown-toggle').click();
diff --git a/addons/mail/static/tests/thread_window_tests.js b/addons/mail/static/tests/thread_window_tests.js
index 620aae407f6..a10bed3f1b0 100644
--- a/addons/mail/static/tests/thread_window_tests.js
+++ b/addons/mail/static/tests/thread_window_tests.js
@@ -40,7 +40,6 @@ QUnit.module('mail', {
self.services.mail_service.prototype.THREAD_WINDOW_APPENDTO = '#qunit-fixture';
}
- params.services = _.clone(params.services);
testUtils.addMockEnvironment(widget, params);
return widget;
};
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 6a96036f5f5..34dd24b2be6 100644
--- a/addons/web/static/src/js/core/abstract_storage_service.js
+++ b/addons/web/static/src/js/core/abstract_storage_service.js
@@ -29,8 +29,9 @@ var AbstractStorageService = AbstractService.extend({
* @param {string} key
* @returns {string}
*/
- getItem: function(key) {
- return this.storage.getItem(key);
+ getItem: function(key, defaultValue) {
+ var val = this.storage.getItem(key);
+ return val ? JSON.parse(val) : defaultValue;
},
/**
* Removes the given key from the storage
@@ -47,7 +48,14 @@ var AbstractStorageService = AbstractService.extend({
* @param {string} value
*/
setItem: function(key, value) {
- this.storage.setItem(key,value);
+ this.storage.setItem(key, JSON.stringify(value));
+ },
+ /**
+ * Add an handler on storage event
+ *
+ */
+ onStorage: function () {
+ this.storage.on.apply(this.storage, ["storage"].concat(Array.prototype.slice.call(arguments)));
},
});
diff --git a/addons/web/static/src/js/core/local_storage.js b/addons/web/static/src/js/core/local_storage.js
index da8e6714cb3..0ae98498b31 100644
--- a/addons/web/static/src/js/core/local_storage.js
+++ b/addons/web/static/src/js/core/local_storage.js
@@ -2,18 +2,55 @@ odoo.define('web.local_storage', function (require) {
'use strict';
var RamStorage = require('web.RamStorage');
+var mixins = require('web.mixins');
// use a fake localStorage in RAM if the native localStorage is unavailable
// (e.g. private browsing in Safari)
+var storage;
var localStorage = window.localStorage;
try {
var uid = new Date();
localStorage.setItem(uid, uid);
localStorage.removeItem(uid);
+
+ /*
+ * We create an intermediate object in order to triggered the storage on
+ * this object. the localStorage. This simplifies testing and usage as
+ * starages are commutable in services without change. Also, objects
+ * that use storage do not have to know that events go through window,
+ * it's not up to them to handle these cases.
+ */
+ storage = (function () {
+ var storage = Object.create(_.extend({
+ getItem: localStorage.getItem.bind(localStorage),
+ setItem: localStorage.setItem.bind(localStorage),
+ removeItem: localStorage.removeItem.bind(localStorage),
+ clear: localStorage.clear.bind(localStorage),
+ },
+ mixins.EventDispatcherMixin
+ ));
+ storage.init();
+ $(window).on('storage', function (e) {
+ var key = e.originalEvent.key;
+ var newValue = e.originalEvent.newValue;
+ try {
+ JSON.parse(newValue);
+ if (localStorage.getItem(key) === newValue) {
+ storage.trigger('storage', {
+ key: key,
+ newValue: newValue,
+ });
+ }
+ } catch (error) {}
+ });
+ return storage;
+ })();
+
} catch (exception) {
- localStorage = new RamStorage();
+ console.warn('Fail to load localStorage');
+ storage = new RamStorage();
}
-return localStorage;
+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 cea66a10dfa..9bf0fc29f93 100644
--- a/addons/web/static/src/js/core/ram_storage.js
+++ b/addons/web/static/src/js/core/ram_storage.js
@@ -8,13 +8,18 @@ odoo.define('web.RamStorage', function (require) {
*/
var Class = require('web.Class');
+var mixins = require('web.mixins');
-var RamStorage = Class.extend({
+
+var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
/**
* @constructor
*/
init: function () {
- this.storage = Object.create(null);
+ mixins.EventDispatcherMixin.init.call(this);
+ if (!this.storage) {
+ this.clear();
+ }
},
//--------------------------------------------------------------------------
@@ -25,7 +30,7 @@ var RamStorage = Class.extend({
* Removes all data from the storage
*/
clear: function () {
- this.init();
+ this.storage = Object.create(null);
},
/**
* Returns the value associated with a given key in the storage
@@ -43,6 +48,7 @@ var RamStorage = Class.extend({
*/
removeItem: function (key) {
delete this.storage[key];
+ this.trigger('storage', {key: key, newValue: null});
},
/**
* Adds a given key-value pair to the storage, or update the value of the
@@ -53,6 +59,7 @@ var RamStorage = Class.extend({
*/
setItem: function (key, value) {
this.storage[key] = value;
+ this.trigger('storage', {key: key, newValue: value});
},
});
diff --git a/addons/web/static/src/js/core/service_mixins.js b/addons/web/static/src/js/core/service_mixins.js
index 170d5dd098a..b190d50a08d 100644
--- a/addons/web/static/src/js/core/service_mixins.js
+++ b/addons/web/static/src/js/core/service_mixins.js
@@ -135,7 +135,8 @@ var ServicesMixin = {
var query = rpc.buildQuery(params);
var def = this.call('ajax', 'rpc', query.route, query.params, options) || $.Deferred();
var promise = def.promise();
- promise.abort = def.abort ? def.abort.bind(def) : function abort () {def.reject();};
+ var abort = (def.abort ? def.abort : def.reject) || function () {};
+ promise.abort = abort.bind(def);
return promise;
},
loadFieldView: function (dataset, view_id, view_type, options) {
diff --git a/addons/web/static/src/js/core/session_storage.js b/addons/web/static/src/js/core/session_storage.js
index f648cee084a..c0787f4d540 100644
--- a/addons/web/static/src/js/core/session_storage.js
+++ b/addons/web/static/src/js/core/session_storage.js
@@ -2,18 +2,55 @@ odoo.define('web.sessionStorage', function (require) {
'use strict';
var RamStorage = require('web.RamStorage');
+var mixins = require('web.mixins');
// use a fake sessionStorage in RAM if the native sessionStorage is unavailable
// (e.g. private browsing in Safari)
+var storage;
var sessionStorage = window.sessionStorage;
try {
var uid = new Date();
sessionStorage.setItem(uid, uid);
sessionStorage.removeItem(uid);
+
+ /*
+ * We create an intermediate object in order to triggered the storage on
+ * this object. the sessionStorage. This simplifies testing and usage as
+ * starages are commutable in services without change. Also, objects
+ * that use storage do not have to know that events go through window,
+ * it's not up to them to handle these cases.
+ */
+ storage = (function () {
+ var storage = Object.create(_.extend({
+ getItem: sessionStorage.getItem.bind(sessionStorage),
+ setItem: sessionStorage.setItem.bind(sessionStorage),
+ removeItem: sessionStorage.removeItem.bind(sessionStorage),
+ clear: sessionStorage.clear.bind(sessionStorage),
+ },
+ mixins.EventDispatcherMixin
+ ));
+ storage.init();
+ $(window).on('storage', function (e) {
+ var key = e.originalEvent.key;
+ var newValue = e.originalEvent.newValue;
+ try {
+ JSON.parse(newValue);
+ if (sessionStorage.getItem(key) === newValue) {
+ storage.trigger('storage', {
+ key: key,
+ newValue: newValue,
+ });
+ }
+ } catch (error) {}
+ });
+ return storage;
+ })();
+
} catch (exception) {
- sessionStorage = new RamStorage();
+ console.warn('Fail to load sessionStorage');
+ storage = new RamStorage();
}
-return sessionStorage;
+return storage;
});
diff --git a/addons/web_editor/static/src/js/root_widget.js b/addons/web_editor/static/src/js/root_widget.js
index 98a14908f10..462dcd9bae9 100644
--- a/addons/web_editor/static/src/js/root_widget.js
+++ b/addons/web_editor/static/src/js/root_widget.js
@@ -4,6 +4,7 @@ odoo.define('web_editor.root_widget', function (require) {
var Class = require('web.Class');
var dom = require('web.dom');
var mixins = require('web.mixins');
+var session = require('web.session');
var Widget = require('web.Widget');
/**
@@ -15,13 +16,13 @@ var Widget = require('web.Widget');
var RootWidget = Widget.extend({
custom_events: _.extend({}, Widget.prototype.custom_events || {}, {
registry_update: '_onRegistryUpdate',
+ get_session: '_onGetSession',
}),
/**
* @constructor
*/
init: function () {
this._super.apply(this, arguments);
-
this._widgets = [];
this._listenToUpdates = false;
this._getRegistry().setParent(this);
@@ -97,6 +98,17 @@ var RootWidget = Widget.extend({
// Handlers
//--------------------------------------------------------------------------
+ /**
+ * Get the curuent session module.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onGetSession: function (event) {
+ if (event.data.callback) {
+ event.data.callback(session);
+ }
+ },
/**
* Called when the linked registry is updated after this `RootWidget`
*
diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml
index b8d77cbd4e5..f71721d92b5 100644
--- a/addons/website_livechat/views/website_livechat.xml
+++ b/addons/website_livechat/views/website_livechat.xml
@@ -11,7 +11,11 @@
-
+
+
+
+
+