diff --git a/addons/bus/static/src/js/services/bus_service.js b/addons/bus/static/src/js/services/bus_service.js
index 7dc932d4575..39b61da51f1 100644
--- a/addons/bus/static/src/js/services/bus_service.js
+++ b/addons/bus/static/src/js/services/bus_service.js
@@ -4,6 +4,7 @@ odoo.define('bus.BusService', function (require) {
var CrossTab = require('bus.CrossTab');
var core = require('web.core');
var ServicesMixin = require('web.ServicesMixin');
+const session = require('web.session');
var BusService = CrossTab.extend(ServicesMixin, {
dependencies : ['local_storage'],
@@ -11,6 +12,38 @@ var BusService = CrossTab.extend(ServicesMixin, {
// properties
_audio: null,
+ /**
+ * As the BusService doesn't extend AbstractService, we have to replicate
+ * here what is done in AbstractService
+ *
+ * @param {Object} env
+ */
+ init: function (env) {
+ this.env = env;
+ this._super();
+ },
+
+ /**
+ * Replicate the behavior of AbstractService:
+ *
+ * Directly calls the requested service, instead of triggering a
+ * 'call_service' event up, which wouldn't work as services have no parent.
+ *
+ * @param {OdooEvent} ev
+ */
+ _trigger_up: function (ev) {
+ if (ev.name === 'call_service') {
+ const payload = ev.data;
+ let args = payload.args || [];
+ if (payload.service === 'ajax' && payload.method === 'rpc') {
+ // ajax service uses an extra 'target' argument for rpc
+ args = args.concat(ev.target);
+ }
+ const service = this.env.services[payload.service];
+ const result = service[payload.method].apply(service, args);
+ payload.callback(result);
+ }
+ },
/**
* This method is necessary in order for this Class to be used to instantiate services
*
@@ -77,7 +110,6 @@ var BusService = CrossTab.extend(ServicesMixin, {
if (!this._audio) {
this._audio = new Audio();
var ext = this._audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
- var session = this.getSession();
this._audio.src = session.url("/mail/static/src/audio/ting" + ext);
}
Promise.resolve(this._audio.play()).catch(_.noop);
diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js
index 04cc696d401..f52af585f94 100644
--- a/addons/mail/static/src/js/services/mail_manager.js
+++ b/addons/mail/static/src/js/services/mail_manager.js
@@ -1004,7 +1004,7 @@ var MailManager = AbstractService.extend({
_t("%d Messages"),
this._outOfFocusUnreadMessageCounter
);
- this.trigger_up('set_title_part', {
+ this.env.bus.trigger('set_title_part', {
part: '_chat',
title: tabTitle
});
@@ -1048,12 +1048,14 @@ var MailManager = AbstractService.extend({
* @param {string} viewID
*/
_redirectToDocument: function (resModel, resID, viewID) {
- this.do_action({
- type: 'ir.actions.act_window',
- view_mode: 'form',
- res_model: resModel,
- views: [[viewID || false, 'form']],
- res_id: resID,
+ this.env.bus.trigger('do-action', {
+ action: {
+ type: 'ir.actions.act_window',
+ view_mode: 'form',
+ res_model: resModel,
+ views: [[viewID || false, 'form']],
+ res_id: resID,
+ },
});
},
/**
@@ -1345,7 +1347,7 @@ var MailManager = AbstractService.extend({
*/
_onWindowFocus: function () {
this._resetOutOfFocusUnreadMessageCounter();
- this.trigger_up('set_title_part', { part: '_chat' });
+ this.env.bus.trigger('set_title_part', { part: '_chat' });
},
});
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 0cabba1d23d..95d9868b89a 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -20,7 +20,6 @@ var data_manager = require('web.data_manager');
var dom = require('web.dom');
var KeyboardNavigationMixin = require('web.KeyboardNavigationMixin');
var Loading = require('web.Loading');
-var local_storage = require('web.local_storage');
var RainbowMan = require('web.RainbowMan');
var session = require('web.session');
var utils = require('web.utils');
@@ -103,7 +102,7 @@ var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
this.action_mutex = new concurrency.Mutex();
this.set('title_part', {"zopenerp": "Odoo"});
this.env = env;
- core.bus.on('legacy_webclient_request', this, this._onLegacyWebclientRequest);
+ this.env.bus.on('set_title_part', this, this._onSetTitlePart);
},
/**
* @override
@@ -206,6 +205,11 @@ var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
set_action_manager: function () {
var self = this;
this.action_manager = new ActionManager(this, session.user_context);
+ this.env.bus.on('do-action', this, payload => {
+ this.action_manager.doAction(payload.action, payload.options)
+ .then(payload.on_success || (() => {}))
+ .guardedCatch(payload.on_fail || (() => {}));
+ });
var fragment = document.createDocumentFragment();
return this.action_manager.appendTo(fragment).then(function () {
dom.append(self.$el, fragment, {
@@ -439,20 +443,6 @@ var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
_onGetScrollPosition: function (ev) {
ev.data.callback(this.getScrollPosition());
},
- /**
- * Services used to trigger_up some events (e.g. do_action, get_session,
- * set_title_part) when the webclient was still instantiating them. Now that
- * services have been moved to the env, the webclient is no longer their
- * parent and they can thus no longer communicate with the webclient by
- * trigger_up. Instead, they trigger those events on the bus, and we end up
- * in this handler. Eventually, services should stop triggering events up.
- *
- * @private
- * @param {CustomEvent} ev
- */
- _onLegacyWebclientRequest: function (ev) {
- this._trigger_up(ev);
- },
/**
* Loads an action from the database given its ID.
*
@@ -502,13 +492,13 @@ var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
},
/**
* @private
- * @param {OdooEvent} ev
- * @param {string} ev.data.part
- * @param {string} [ev.data.title]
+ * @param {Object} payload
+ * @param {string} payload.part
+ * @param {string} [payload.title]
*/
- _onSetTitlePart: function (ev) {
- var part = ev.data.part;
- var title = ev.data.title;
+ _onSetTitlePart: function (payload) {
+ var part = payload.part;
+ var title = payload.title;
this.set_title_part(part, title);
},
/**
diff --git a/addons/web/static/src/js/common_env.js b/addons/web/static/src/js/common_env.js
new file mode 100644
index 00000000000..0e31b8e3ceb
--- /dev/null
+++ b/addons/web/static/src/js/common_env.js
@@ -0,0 +1,119 @@
+odoo.define("web.commonEnv", function (require) {
+ "use strict";
+
+ /**
+ * This file defines the common environment, which contains everything that
+ * is needed in the env for both the backend and the frontend (Odoo
+ * terminology). This module shouldn't be used as is. It should only be
+ * imported by the module defining the final env to use (in the frontend or
+ * in the backend). For instance, module 'web.env' imports it, adds stuff to
+ * it, and exports the final env that is used by the whole webclient
+ * application.
+ *
+ * There should be as much dependencies as possible in the env object. This
+ * will allow an easier testing of components. See [1] for more information
+ * on environments.
+ *
+ * [1] https://github.com/odoo/owl/blob/master/doc/reference/environment.md#content-of-an-environment
+ */
+
+ const { jsonRpc } = require("web.ajax");
+ const { device, isDebug } = require("web.config");
+ const { bus, serviceRegistry } = require("web.core");
+ const rpc = require("web.rpc");
+ const session = require("web.session");
+ const { _t } = require("web.translation");
+ const utils = require("web.utils");
+
+
+ // Build the basic env
+ const env = {
+ _t,
+ bus,
+ device,
+ isDebug,
+ qweb: new owl.QWeb({ translateFn: _t }),
+ services: {
+ ajaxJsonRPC() {
+ return jsonRpc(...arguments);
+ },
+ getCookie() {
+ return utils.get_cookie(...arguments);
+ },
+ httpRequest(route, params = {}, readMethod = 'json') {
+ const info = {
+ method: params.method || 'POST',
+ };
+ if (params.method !== 'GET') {
+ const formData = new FormData();
+ for (const key in params) {
+ if (key === 'method') {
+ continue;
+ }
+ const value = params[key];
+ if (Array.isArray(value) && value.length) {
+ for (const val of value) {
+ formData.append(key, val);
+ }
+ } else {
+ formData.append(key, value);
+ }
+ }
+ info.body = formData;
+ }
+ return fetch(route, info).then(response => response[readMethod]());
+ },
+ navigate(url, params) {
+ window.location = $.param.querystring(url, params);
+ },
+ reloadPage() {
+ window.location.reload();
+ },
+ rpc(params, options) {
+ const query = rpc.buildQuery(params);
+ return session.rpc(query.route, query.params, options);
+ },
+ setCookie() {
+ utils.set_cookie(...arguments);
+ },
+ },
+ session,
+ };
+
+
+ // Deploy services in the env (specializations of AbstractService registered
+ // into the serviceRegistry)
+ const UndeployedServices = Object.assign({}, serviceRegistry.map);
+ function _deployServices() {
+ let done = false;
+ while (!done) {
+ // find a service with no missing dependency
+ const serviceName = Object.keys(UndeployedServices).find(serviceName => {
+ const Service = UndeployedServices[serviceName];
+ return Service.prototype.dependencies.every(depName => {
+ return env.services[depName];
+ });
+ });
+ if (serviceName) {
+ const Service = UndeployedServices[serviceName];
+ const service = new Service(env);
+ env.services[serviceName] = service;
+ delete UndeployedServices[serviceName];
+ service.start();
+ } else {
+ done = true;
+ }
+ }
+ }
+ serviceRegistry.onAdd((serviceName, Service) => {
+ if (serviceName in env.services || serviceName in UndeployedServices) {
+ throw new Error(`Service ${serviceName} is already loaded.`);
+ }
+ UndeployedServices[serviceName] = Service;
+ _deployServices();
+ });
+ _deployServices();
+
+
+ return env;
+});
diff --git a/addons/web/static/src/js/core/abstract_service.js b/addons/web/static/src/js/core/abstract_service.js
index c907b28f7c8..f9ad859fef2 100644
--- a/addons/web/static/src/js/core/abstract_service.js
+++ b/addons/web/static/src/js/core/abstract_service.js
@@ -7,14 +7,39 @@ var ServicesMixin = require('web.ServicesMixin');
var AbstractService = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
dependencies: [],
- init: function (parent) {
+ init: function (env) {
Mixins.EventDispatcherMixin.init.call(this, arguments);
- this.setParent(parent);
+ this.env = env;
},
/**
* @abstract
*/
start: function () {},
+ /**
+ * Directly calls the requested service, instead of triggering a
+ * 'call_service' event up, which wouldn't work as services have no parent
+ *
+ * @param {OdooEvent} ev
+ */
+ _trigger_up: function (ev) {
+ Mixins.EventDispatcherMixin._trigger_up.apply(this, arguments);
+ if (ev.is_stopped()) {
+ return;
+ }
+ const payload = ev.data;
+ if (ev.name === 'call_service') {
+ let args = payload.args || [];
+ if (payload.service === 'ajax' && payload.method === 'rpc') {
+ // ajax service uses an extra 'target' argument for rpc
+ args = args.concat(ev.target);
+ }
+ const service = this.env.services[payload.service];
+ const result = service[payload.method].apply(service, args);
+ payload.callback(result);
+ } else if (ev.name === 'do_action') {
+ this.env.bus.trigger('do-action', payload);
+ }
+ },
});
return AbstractService;
diff --git a/addons/web/static/src/js/core/service_mixins.js b/addons/web/static/src/js/core/service_mixins.js
index f191dba5c08..198d00993ed 100644
--- a/addons/web/static/src/js/core/service_mixins.js
+++ b/addons/web/static/src/js/core/service_mixins.js
@@ -3,6 +3,8 @@ odoo.define('web.ServiceProviderMixin', function (require) {
var core = require('web.core');
+// ServiceProviderMixin is deprecated. It is only used by the ProjectTimesheet
+// app. As soon as it no longer uses it, we can remove it.
var ServiceProviderMixin = {
services: {}, // dict containing deployed service instances
UndeployedServices: {}, // dict containing classes of undeployed services
diff --git a/addons/web/static/src/js/env.js b/addons/web/static/src/js/env.js
index c95cbd3045a..7fd7e1defab 100644
--- a/addons/web/static/src/js/env.js
+++ b/addons/web/static/src/js/env.js
@@ -1,157 +1,16 @@
odoo.define("web.env", function (require) {
"use strict";
- const { jsonRpc } = require('web.ajax');
- const { device, isDebug } = require("web.config");
- const { bus, serviceRegistry } = require("web.core");
+ /**
+ * This file defines the env to use in the webclient.
+ */
+
+ const commonEnv = require('web.commonEnv');
const dataManager = require('web.data_manager');
const { blockUI, unblockUI } = require("web.framework");
- const rpc = require("web.rpc");
- const session = require("web.session");
- const { _t } = require('web.translation');
- const utils = require("web.utils");
- const qweb = new owl.QWeb({ translateFn: _t });
+ const env = Object.assign(commonEnv, { dataManager });
+ env.services = Object.assign(env.services, { blockUI, unblockUI });
- function ajaxJsonRPC() {
- return jsonRpc(...arguments);
- }
-
- function getCookie() {
- return utils.get_cookie(...arguments);
- }
-
- function httpRequest(route, params = {}, readMethod = 'json') {
- const info = {
- method: params.method || 'POST',
- };
- if (params.method !== 'GET') {
- const formData = new FormData();
- for (const key in params) {
- if (key === 'method') {
- continue;
- }
- const value = params[key];
- if (Array.isArray(value) && value.length) {
- for (const val of value) {
- formData.append(key, val);
- }
- } else {
- formData.append(key, value);
- }
- }
- info.body = formData;
- }
-
- return fetch(route, info).then(response => response[readMethod]());
- }
-
- function navigate(url, params) {
- window.location = $.param.querystring(url, params);
- }
-
- function performRPC(params, options) {
- const query = rpc.buildQuery(params);
- return session.rpc(query.route, query.params, options);
- }
-
- function reloadPage() {
- window.location.reload();
- }
-
- function setCookie() {
- utils.set_cookie(...arguments);
- }
-
- // ServiceProvider
- const services = {}; // dict containing deployed service instances
- const UndeployedServices = {}; // dict containing classes of undeployed services
- function _deployServices() {
- let done = false;
- while (!done) {
- const serviceName = _.findKey(UndeployedServices, Service => {
- // no missing dependency
- return !_.some(Service.prototype.dependencies, depName => {
- return !services[depName];
- });
- });
- if (serviceName) {
- const Service = UndeployedServices[serviceName];
- // we created a patched version of the Service in which the 'trigger_up'
- // function directly calls the requested service, instead of triggering
- // a 'call_service' event up, which wouldn't work as services have
- // no parent
- const PatchedService = Service.extend({
- _trigger_up: function (ev) {
- this._super(...arguments);
- if (!ev.is_stopped() && ev.name === 'call_service') {
- const payload = ev.data;
- let args = payload.args || [];
- if (payload.service === 'ajax' && payload.method === 'rpc') {
- // ajax service uses an extra 'target' argument for rpc
- args = args.concat(ev.target);
- }
- const service = services[payload.service];
- const result = service[payload.method].apply(service, args);
- payload.callback(result);
- } else {
- // historically, some services could reach the webclient
- // by triggering events up, as the webclient was their
- // parent. Since services have been moved to the env,
- // this is no longer the case, so we re-trigger those
- // events on the bus for now. Eventually, services
- // should stop triggering events up (they can still
- // communicate with other services through the env).
- bus.trigger('legacy_webclient_request', ev);
- }
- },
- });
- const service = new PatchedService();
- services[serviceName] = service;
- delete UndeployedServices[serviceName];
- service.start();
- } else {
- done = true;
- }
- }
- }
- _.each(serviceRegistry.map, (Service, serviceName) => {
- if (serviceName in UndeployedServices) {
- throw new Error(`Service ${serviceName} is already loaded.`);
- }
- UndeployedServices[serviceName] = Service;
- });
- serviceRegistry.onAdd((serviceName, Service) => {
- if (serviceName in services || serviceName in UndeployedServices) {
- throw new Error(`Service ${serviceName} is already loaded.`);
- }
- UndeployedServices[serviceName] = Service;
- _deployServices();
- });
- _deployServices();
-
- // There should be as much dependencies as possible in the env object.
- // This will allow an easier testing of components.
- // See https://github.com/odoo/owl/blob/master/doc/reference/environment.md#content-of-an-environment
- // for more information on environments.
- return {
- _t,
- bus,
- dataManager,
- device,
- isDebug,
- qweb,
- services: Object.assign(services, {
- ajaxJsonRPC,
- blockUI,
- getCookie,
- httpRequest,
- navigate,
- reloadPage,
- rpc: performRPC,
- setCookie,
- unblockUI,
- }),
- session,
- };
+ return env;
});
diff --git a/addons/web/static/src/js/public/public_env.js b/addons/web/static/src/js/public/public_env.js
index 373c7b512b8..581cccd430b 100644
--- a/addons/web/static/src/js/public/public_env.js
+++ b/addons/web/static/src/js/public/public_env.js
@@ -1,24 +1,11 @@
odoo.define("web.public_env", function (require) {
"use strict";
- const { _t } = require("web.core");
- const rpc = require("web.rpc");
+ /**
+ * This file defines the env to use in the public side.
+ */
- const qweb = new owl.QWeb({ translateFn: _t });
+ const commonEnv = require("web.commonEnv");
- function performRPC(params, options) {
- const query = rpc.buildQuery(params);
- return session.rpc(query.route, query.params, options);
- }
-
- // There should be as much dependencies as possible in the env object.
- // This will allow an easier testing of components.
- // See https://github.com/odoo/owl/blob/master/doc/reference/environment.md#content-of-an-environment
- // for more information on environments.
- return {
- qweb,
- services: {
- rpc: performRPC,
- },
- };
+ return commonEnv;
});
diff --git a/addons/web/static/src/js/public/public_root.js b/addons/web/static/src/js/public/public_root.js
index 33d442e1d76..f684df2b629 100644
--- a/addons/web/static/src/js/public/public_root.js
+++ b/addons/web/static/src/js/public/public_root.js
@@ -3,7 +3,7 @@ odoo.define('web.public.root', function (require) {
var ajax = require('web.ajax');
var dom = require('web.dom');
-var ServiceProviderMixin = require('web.ServiceProviderMixin');
+const env = require('web.public_env');
var session = require('web.session');
var utils = require('web.utils');
var publicWidget = require('web.public.widget');
@@ -25,16 +25,17 @@ var localeDef = ajax.loadJS('/web/webclient/locale/' + lang.replace('-', '_'));
* this Class instance. Its main role will be to retrieve RPC demands from its
* children and handle them.
*/
-var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
+var PublicRoot = publicWidget.RootWidget.extend({
events: _.extend({}, publicWidget.RootWidget.prototype.events || {}, {
'submit .js_website_submit_form': '_onWebsiteFormSubmit',
'click .js_disable_on_click': '_onDisableOnClick',
}),
custom_events: _.extend({}, publicWidget.RootWidget.prototype.custom_events || {}, {
- 'context_get': '_onContextGet',
- 'main_object_request': '_onMainObjectRequest',
- 'widgets_start_request': '_onWidgetsStartRequest',
- 'widgets_stop_request': '_onWidgetsStopRequest',
+ call_service: '_onCallService',
+ context_get: '_onContextGet',
+ main_object_request: '_onMainObjectRequest',
+ widgets_start_request: '_onWidgetsStartRequest',
+ widgets_stop_request: '_onWidgetsStopRequest',
}),
/**
@@ -42,7 +43,7 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
*/
init: function () {
this._super.apply(this, arguments);
- ServiceProviderMixin.init.call(this);
+ this.env = env;
this.publicWidgets = [];
},
/**
@@ -90,37 +91,6 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
// Private
//--------------------------------------------------------------------------
- /**
- * Automatically adds the global context to RPCs.
- *
- * @override
- */
- _call_service: function (ev) {
- if (ev.data.service === 'ajax' && ev.data.method === 'rpc') {
- var route = ev.data.args[0];
- if (_.str.startsWith(route, '/web/dataset/call_kw/')) {
- var params = ev.data.args[1];
- var options = ev.data.args[2];
- var noContextKeys = undefined;
- if (options) {
- noContextKeys = options.noContextKeys;
- ev.data.args[2] = _.omit(options, 'noContextKeys');
- }
- params.kwargs.context = _computeContext.call(this, params.kwargs.context, noContextKeys);
- }
- } else if (ev.data.service === 'ajax' && ev.data.method === 'loadLibs') {
- ev.data.args[1] = _computeContext.call(this, ev.data.args[1]);
- }
- return ServiceProviderMixin._call_service.apply(this, arguments);
-
- function _computeContext(context, noContextKeys) {
- context = _.extend({}, this._getContext(), context);
- if (noContextKeys) {
- context = _.omit(context, noContextKeys);
- }
- return JSON.parse(JSON.stringify(context));
- }
- },
/**
* Retrieves the global context of the public environment. This is the
* context which is automatically added to each RPC.
@@ -234,6 +204,47 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
// Handlers
//--------------------------------------------------------------------------
+ /**
+ * Calls the requested service from the env. Automatically adds the global
+ * context to RPCs.
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onCallService: function (ev) {
+ function _computeContext(context, noContextKeys) {
+ context = _.extend({}, this._getContext(), context);
+ if (noContextKeys) {
+ context = _.omit(context, noContextKeys);
+ }
+ return JSON.parse(JSON.stringify(context));
+ }
+
+ const payload = ev.data;
+ let args = payload.args || [];
+ if (payload.service === 'ajax' && payload.method === 'rpc') {
+ // ajax service uses an extra 'target' argument for rpc
+ args = args.concat(ev.target);
+
+ var route = args[0];
+ if (_.str.startsWith(route, '/web/dataset/call_kw/')) {
+ var params = args[1];
+ var options = args[2];
+ var noContextKeys;
+ if (options) {
+ noContextKeys = options.noContextKeys;
+ args[2] = _.omit(options, 'noContextKeys');
+ }
+ params.kwargs.context = _computeContext.call(this, params.kwargs.context, noContextKeys);
+ }
+ } else if (payload.service === 'ajax' && payload.method === 'loadLibs') {
+ args[1] = _computeContext.call(this, args[1]);
+ }
+
+ const service = this.env.services[payload.service];
+ const result = service[payload.method].apply(service, args);
+ payload.callback(result);
+ },
/**
* Called when someone asked for the global public context.
*
diff --git a/addons/web/static/src/js/public/public_root_instance.js b/addons/web/static/src/js/public/public_root_instance.js
index 988d939bbe1..e368d421fbe 100644
--- a/addons/web/static/src/js/public/public_root_instance.js
+++ b/addons/web/static/src/js/public/public_root_instance.js
@@ -1,9 +1,16 @@
odoo.define('root.widget', function (require) {
'use strict';
+const env = require('web.public_env');
var lazyloader = require('web.public.lazyloader');
var rootData = require('web.public.root');
+/**
+ * Configure Owl with the public env
+ */
+owl.config.mode = env.isDebug() ? "dev" : "prod";
+owl.Component.env = env;
+
/**
* This widget is important, because the tour manager needs a root widget in
* order to work. The root widget must be a service provider with the ajax
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index bebc3255dc2..3cb081e7e37 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -167,6 +167,8 @@
+
+