summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/env.js71
-rw-r--r--addons/web/static/src/js/owl_compatibility.js258
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,
+ };
+});