diff options
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/env.js | 71 | ||||
| -rw-r--r-- | addons/web/static/src/js/owl_compatibility.js | 258 |
2 files changed, 323 insertions, 6 deletions
diff --git a/addons/web/static/src/js/env.js b/addons/web/static/src/js/env.js index c223fcc91ee..72d14ae3e60 100644 --- a/addons/web/static/src/js/env.js +++ b/addons/web/static/src/js/env.js @@ -1,15 +1,15 @@ odoo.define("web.env", function (require) { "use strict"; - const { _lt, _t, bus } = require("web.core"); - const { blockUI, unblockUI } = require("web.framework"); - const { device, isDebug } = require("web.config"); const { jsonRpc } = require('web.ajax'); + const { device, isDebug } = require("web.config"); + const { _lt, _t, bus, serviceRegistry } = require("web.core"); + const dataManager = require('web.data_manager'); + const { blockUI, unblockUI } = require("web.framework"); const rpc = require("web.rpc"); const session = require("web.session"); const utils = require("web.utils"); - const qweb = new owl.QWeb({ translateFn: _t }); function ajaxJsonRPC() { @@ -59,6 +59,64 @@ odoo.define("web.env", function (require) { 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); + } + }, + }); + 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 @@ -67,10 +125,11 @@ odoo.define("web.env", function (require) { _lt, _t, bus, + dataManager, device, isDebug, qweb, - services: { + services: Object.assign(services, { ajaxJsonRPC, blockUI, getCookie, @@ -80,7 +139,7 @@ odoo.define("web.env", function (require) { rpc: performRPC, setCookie, unblockUI, - }, + }), session, }; }); diff --git a/addons/web/static/src/js/owl_compatibility.js b/addons/web/static/src/js/owl_compatibility.js new file mode 100644 index 00000000000..e04e42c30b5 --- /dev/null +++ b/addons/web/static/src/js/owl_compatibility.js @@ -0,0 +1,258 @@ +odoo.define('web.OwlCompatibility', function () { + "use strict"; + + /** + * This file defines the necessary tools for the transition phase where Odoo + * legacy widgets and Owl components will coexist. There are two possible + * scenarios: + * 1) An Owl component has to instantiate legacy widgets + * 2) A legacy widget has to instantiate Owl components + */ + + const { Component, tags } = owl; + + /** + * Case 1) An Owl component has to instantiate legacy widgets + * ---------------------------------------------------------- + * + * The ComponentAdapter is an Owl component meant to be used as universal + * adapter for Owl components that embed Odoo legacy widgets (or dynamically + * both Owl components and Odoo legacy widgets), e.g.: + * + * Owl Component + * | + * ComponentAdapter (Owl component) + * | + * Legacy Widget(s) (or Owl component(s)) + * + * + * The adapter takes the component/widget class as 'Component' prop, and the + * arguments (except first arg 'parent') to initialize it as props. + * For instance: + * <ComponentAdapter Component="LegacyWidget" params="params"/> + * will be translated to: + * const LegacyWidget = this.props.Component; + * const legacyWidget = new LegacyWidget(this, this.props.params); + * + * If more than one argument (in addition to 'parent') is given to initialize + * the legacy widget, the arguments order (to initialize the sub widget) has + * to be somehow specified. There are two alternatives. One can either (1) + * specify the prop 'widgetArgs', corresponding to the array of arguments, + * otherwise (2) a subclass of ComponentAdapter has to be defined. This + * subclass must override the 'widgetArgs' getter to translate arguments + * received as props to an array of arguments for the call to init. + * For instance: + * (1) <ComponentAdapter Component="LegacyWidget" firstArg="a" secondArg="b" widgetsArgs="[a, b]"/> + * (2) class SpecificAdapter extends ComponentAdapter { + * get widgetArgs() { + * return [this.props.firstArg, this.props.secondArg]; + * } + * } + * <SpecificAdapter Component="LegacyWidget" firstArg="a" secondArg="b"/> + * + * If the legacy widget has to be updated when props change, one must define + * a subclass of ComponentAdapter to override 'update' and 'render'. The + * 'update' function takes the nextProps as argument, and should update the + * internal state of the widget (might be async, and return a Promise). + * However, to ensure that the DOM is updated all at once, it shouldn't do + * a re-rendering. This is the role of function 'render', which will be + * called just before patching the DOM, and which thus must be synchronous. + * For instance: + * class SpecificAdapter extends ComponentAdapter { + * update(nextProps) { + * return this.widget.updateState(nextProps); + * } + * render() { + * return this.widget.render(); + * } + * } + */ + class ComponentAdapter extends Component { + /** + * Creates the template on-the-fly, depending on the type of Component + * (legacy widget or Owl component). + * + * @override + */ + constructor(parent, props) { + if (!props.Component) { + throw Error(`ComponentAdapter: 'Component' prop is missing.`); + } + let template; + if (!(props.Component.prototype instanceof Component)) { + template = tags.xml`<div/>`; + } else { + let propsStr = ''; + for (let p in props) { + if (p !== 'Component') { + propsStr += ` ${p}="props.${p}"`; + } + } + template = tags.xml`<t t-component="props.Component"${propsStr}/>`; + } + ComponentAdapter.template = template; + super(...arguments); + this.template = template; + ComponentAdapter.template = null; + + this.widget = null; // widget instance, if Component is a legacy widget + } + + /** + * Starts the legacy widget (not in the DOM yet) + * + * @override + */ + willStart() { + if (!(this.props.Component.prototype instanceof Component)) { + this.widget = new this.props.Component(this, ...this.widgetArgs); + return this.widget._widgetRenderAndInsert(() => {}); + } + } + + /** + * Updates the internal state of the legacy widget (but doesn't re-render + * it yet). + * + * @override + */ + willUpdateProps(nextProps) { + if (this.widget) { + return this.update(nextProps); + } + } + + /** + * Hooks just before the actual patch to replace the fake div in the + * vnode by the actual node of the legacy widget. If the widget has to + * be re-render (because it has previously been updated), re-render it. + * This must be synchronous. + * + * @override + */ + __patch(vnode) { + if (this.widget) { + if (this.__owl__.vnode) { // not at first rendering + this.render(); + } + vnode.elm = this.widget.el; + } + return super.__patch(...arguments); + } + + /** + * @override + */ + mounted() { + if (this.widget && this.widget.on_attach_callback) { + this.widget.on_attach_callback(); + } + } + + /** + * @override + */ + willUnmount() { + if (this.widget && this.widget.on_detach_callback) { + this.widget.on_detach_callback(); + } + } + + /** + * @override + */ + __destroy() { + super.__destroy(...arguments); + if (this.widget) { + this.widget.destroy(); + } + } + + /** + * Getter that translates the props (except 'Component') into the array + * of arguments used to initialize the legacy widget. + * + * Must be overriden if at least two props (other that Component) are + * given. + * + * @returns {Array} + */ + get widgetArgs() { + if (this.props.widgetArgs) { + return this.props.widgetArgs; + } + const args = Object.keys(this.props); + args.splice(args.indexOf('Component'), 1); + if (args.length > 1) { + throw new Error(`ComponentAdapter has more than 1 argument, 'widgetArgs' must be overriden.`); + } + return args.map(a => this.props[a]); + } + + /** + * Can be overriden to update the internal state of the widget when props + * change. To ensure that the DOM is updated at once, this function should + * not do a re-rendering (which should be done by 'render' instead). + * + * @param {Object} nextProps + * @returns {Promise} + */ + update(/*nextProps*/) { + console.warn(`ComponentAdapter: Widget could not be updated, maybe override 'update' function?`); + } + + /** + * Can be overriden to re-render the widget after an update. This + * function will be called just before patchin the DOM, s.t. the DOM is + * updated at once. It must be synchronous + */ + render() { + console.warn(`ComponentAdapter: Widget could not be re-rendered, maybe override 'render' function?`); + } + + /** + * Mocks _trigger_up to redirect Odoo legacy events to OWL events. + * + * @private + * @param {OdooEvent} ev + */ + _trigger_up(ev) { + const evType = ev.name; + const payload = ev.data; + if (evType === '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 (evType === 'get_session') { + if (payload.callback) { + payload.callback(this.env.session); + } + } else if (evType === 'load_views') { + const params = { + model: payload.modelName, + context: payload.context, + views_descr: payload.views, + }; + this.env.dataManager + .load_views(params, payload.options || {}) + .then(payload.on_success); + } else if (evType === 'load_filters') { + return this.env.dataManager + .load_filters(payload) + .then(payload.on_success); + } else { + payload.__targetWidget = ev.target; + this.trigger(evType.replace(/_/g, '-'), payload); + } + } + } + + return { + ComponentAdapter, + }; +}); |
