From 26fe23201bb495176d14c15c8866e8fc72ca044c Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 28 May 2020 09:55:44 +0000 Subject: [PATCH] [IMP] web,*: properly define env in the frontend *bus,mail This commit extracts the common basis of the env to use both in the frontend and the backend. The public env now contains most of the features that were previously (only) in the webclient env. Moreover, the PublicRoot no longer uses the ServiceProviderMixin, such that services are only deployed once, in the (public) env. Owl components can now be defined in the website, and rely on a properly built env. Legacy widgets still works as they access services through the PublicRoot (via trigger_up) which redirects those requests to the env. --- .../bus/static/src/js/services/bus_service.js | 34 +++- .../static/src/js/services/mail_manager.js | 18 +- .../src/js/chrome/abstract_web_client.js | 34 ++-- addons/web/static/src/js/common_env.js | 119 +++++++++++++ .../static/src/js/core/abstract_service.js | 29 +++- .../web/static/src/js/core/service_mixins.js | 2 + addons/web/static/src/js/env.js | 157 +----------------- addons/web/static/src/js/public/public_env.js | 23 +-- .../web/static/src/js/public/public_root.js | 87 +++++----- .../src/js/public/public_root_instance.js | 7 + addons/web/views/webclient_templates.xml | 2 + 11 files changed, 274 insertions(+), 238 deletions(-) create mode 100644 addons/web/static/src/js/common_env.js 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 @@ + +