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 @@ + +