[IMP] web: simplify owl_compatibility

This commit tried to remove owl_compatibility completely but its last
uses are either in refactoring or a bit complex to migrate. Instead of
removing the helpers completely, this commit simplifies them a lot and
makes them more readable and safe.

closes odoo/odoo#134295

Task: 3439226
Related: odoo/enterprise#46899
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Michael (mcm)
2023-09-08 11:12:39 +00:00
parent 290a9b9055
commit 36061a4ab0
18 changed files with 221 additions and 2532 deletions
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { ComponentWrapper } from "@web/legacy/js/owl_compatibility";
import { attachComponent } from "@web/legacy/utils";
import { Wysiwyg } from "@web_editor/js/wysiwyg/wysiwyg";
import { closestElement } from "@web_editor/js/editor/odoo-editor/src/OdooEditor";
import { Toolbar } from "@web_editor/js/editor/toolbar";
@@ -46,15 +46,13 @@ export class MassMailingWysiwyg extends Wysiwyg {
if (fold) {
this.snippetsMenu.setFolded(true);
if (!this.floatingToolbar) {
// Instantiate and configure new toolbar.
const toolbarWrapper = new ComponentWrapper({}, Toolbar, this.state.toolbarProps);
// The wysiwyg can be instanciated inside an iframe. The dialog
// component is mounted on the global document.
const toolbarWrapperElement = document.createElement('div');
toolbarWrapperElement.style.display = 'contents';
document.body.append(toolbarWrapperElement);
await toolbarWrapper.mount(toolbarWrapperElement);
// Instantiate and configure new toolbar.
await attachComponent({}, toolbarWrapperElement, Toolbar, this.state.toolbarProps);
this.toolbarEl = toolbarWrapperElement.firstChild;
this.floatingToolbarEl = this.toolbarEl;
@@ -69,8 +69,6 @@
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
'web/static/src/libs/owl.js',
'web/static/src/legacy/js/component_extension.js',
'web/static/src/legacy/legacy_component.js',
'web/static/lib/jquery/jquery.js',
'web/static/lib/jquery.ui/jquery-ui.js',
'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js',
@@ -162,7 +160,6 @@
'web/static/src/legacy/backend_utils.js',
'web/static/src/legacy/utils.js',
'web/static/src/legacy/js/env.js',
'web/static/src/legacy/js/owl_compatibility.js',
'mrp_subcontracting/static/src/subcontracting_portal/*',
'web/static/src/start.js',
-1
View File
@@ -142,7 +142,6 @@
# FIXME POSREF legacy dependencies to remove
'web/static/lib/moment/moment.js',
'web/static/src/legacy/js/promise_extension.js',
'web/static/src/legacy/legacy_component.js',
],
# Bundle that starts the pos, loaded on /pos/ui
'point_of_sale.assets_prod': [
-3
View File
@@ -120,8 +120,6 @@
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
'web/static/src/libs/owl.js',
'web/static/src/legacy/js/component_extension.js',
'web/static/src/legacy/legacy_component.js',
'web/static/lib/jquery/jquery.js',
'web/static/lib/jquery.ui/jquery-ui.js',
'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js',
@@ -215,7 +213,6 @@
'web/static/src/legacy/backend_utils.js',
'web/static/src/legacy/utils.js',
'web/static/src/legacy/js/env.js',
'web/static/src/legacy/js/owl_compatibility.js',
'web_editor/static/src/js/editor/odoo-editor/src/base_style.scss',
'web_editor/static/lib/vkbeautify/**/*',
-6
View File
@@ -69,8 +69,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/js/core/cookie_utils.js',
'web/static/lib/moment/moment.js',
'web/static/src/legacy/js/component_extension.js',
'web/static/src/legacy/legacy_component.js',
'web/static/lib/jquery/jquery.js',
'web/static/lib/jquery.ui/jquery-ui.js',
'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js',
@@ -151,7 +149,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/backend_utils.js',
'web/static/src/legacy/utils.js',
'web/static/src/legacy/js/env.js',
'web/static/src/legacy/js/owl_compatibility.js',
'web/static/src/legacy/xml/base.xml',
# Don't include dark mode files in light mode
@@ -206,8 +203,6 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
'web/static/src/libs/owl.js',
'web/static/src/legacy/js/component_extension.js',
'web/static/src/legacy/legacy_component.js',
'web/static/lib/jquery/jquery.js',
'web/static/lib/jquery.ui/jquery-ui.js',
'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js',
@@ -266,7 +261,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/libs/pdfjs.js',
'web/static/src/legacy/utils.js',
'web/static/src/legacy/js/owl_compatibility.js',
'web/static/src/legacy/js/public/public_env.js',
'web/static/src/legacy/js/public/public_root.js',
'web/static/src/legacy/js/public/public_root_instance.js',
@@ -1,10 +0,0 @@
(function () {
owl.Component.env = {};
/**
* Symbol used in ComponentWrapper to redirect Owl events to Odoo legacy
* events.
*/
odoo.widgetSymbol = Symbol('widget');
})();
@@ -1,744 +0,0 @@
/** @odoo-module **/
import { LegacyComponent } from "@web/legacy/legacy_component";
import { templates } from "@web/core/assets";
import { _t } from "@web/core/l10n/translation";
/**
* 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 {
App,
Component,
onMounted,
onWillStart,
onWillUnmount,
onPatched,
onWillUpdateProps,
onWillDestroy,
useSubEnv,
xml,
status,
} = owl;
const widgetSymbol = odoo.widgetSymbol;
const children = new WeakMap(); // associates legacy widgets with their Owl children
const templateForLegacy = xml`<t/>`;
const templateForOwl = xml`<t t-component="props.Component" t-props="childProps" />`;
/**
* 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 'updateWidget' and 'renderWidget'. The
* 'updateWidget' 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 'renderWidget', which will be
* called just before patching the DOM, and which thus must be synchronous.
* For instance:
* class SpecificAdapter extends ComponentAdapter {
* updateWidget(nextProps) {
* return this.widget.updateState(nextProps);
* }
* renderWidget() {
* return this.widget.render();
* }
* }
*/
export class ComponentAdapter extends LegacyComponent {
/**
* Creates the template on-the-fly, depending on the type of Component
* (legacy widget or Owl component).
*
* @override
*/
constructor(props) {
if (!props.Component) {
throw Error(`ComponentAdapter: 'Component' prop is missing.`);
}
let template;
if (!(props.Component.prototype instanceof Component)) {
template = templateForLegacy;
} else {
template = templateForOwl;
}
ComponentAdapter.template = template;
super(...arguments);
this.template = template;
this.widget = null; // widget instance, if Component is a legacy widget
}
setup() {
onWillUpdateProps((nextProps) => {
if (this.widget) {
return this.updateWidget(nextProps);
}
});
let widgetIsAttached = false;
const insertWidget = () => {
this.removeEl();
if (!this.widget || !this.widget.el) {
return;
}
const node = this.__owl__.firstNode();
node.parentNode.insertBefore(this.widget.el, node);
this.widgetEl = this.widget.el;
widgetIsAttached = true;
};
onMounted(() => {
insertWidget();
if (this.widget && this.widget.on_attach_callback) {
this.widget.on_attach_callback();
}
});
onPatched(() => {
if (widgetIsAttached) {
this.renderWidget();
} else {
insertWidget();
}
});
onWillDestroy(() => this.__destroy(this.__owl__.parent.component));
onWillStart(this.onWillStart);
onWillUnmount(this.onWillUnmount);
}
onWillStart() {
if (!(this.props.Component.prototype instanceof Component)) {
this.widget = new this.props.Component(this, ...this.widgetArgs);
return this.widget._widgetRenderAndInsert(() => { });
}
}
onWillUnmount() {
if (this.widget && this.widget.on_detach_callback) {
this.widget.on_detach_callback();
}
this.removeEl();
}
removeEl() {
if (this.widgetEl) {
this.widgetEl.remove();
this.widgetEl = null;
}
}
__destroy() {
this.removeEl();
if (this.widget) {
this.widget.destroy();
}
}
get childProps() {
if (!this._childProps) {
this._childProps = Object.assign({}, this.props);
delete this._childProps.Component;
}
return this._childProps;
}
/**
* 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}
*/
updateWidget(/*nextProps*/) {
if (odoo.debug.includes("assets")) {
console.warn(`ComponentAdapter: Widget could not be updated, maybe override 'updateWidget' 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
*/
renderWidget() {
if (odoo.debug.includes("assets")) {
console.warn(`ComponentAdapter: Widget could not be re-rendered, maybe override 'renderWidget' 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];
//If the service doesn't exist it means that it was translated to Owl
if (service) {
const result = service[payload.method].apply(service, args);
payload.callback(result);
} else {
throw new Error(
`The service "${payload.service}" is not present in the legacy owl environment.
You should probably create a mapper in @web/legacy/utils`
);
}
} else {
payload.__targetWidget = ev.target;
this.trigger(evType.replace(/_/g, '-'), payload);
}
}
get el() {
if (this.widget) {
return this.widget.el;
}
return super.el;
}
}
const bodyRef = { get el() { return document.body } };
export function standaloneAdapter(props = {}, ref = bodyRef) {
const env = owl.Component.env;
const app = new App(null, {
templates,
env,
dev: !!odoo.debug,
translatableAttributes: ["data-tooltip"],
translateFn: _t,
});
if (!("Component" in props)) {
props.Component = owl.Component;
}
const component = app.makeNode(ComponentAdapter, props).component;
Object.defineProperty(component, "el", {
get() {
return ref.el;
}
});
return component;
}
/**
* Case 2) A legacy widget has to instantiate Owl components
* ---------------------------------------------------------
*
* The WidgetAdapterMixin and the ComponentWrapper are meant to be used
* together when an Odoo legacy widget needs to instantiate Owl components.
* In this case, the widgets/components hierarchy would look like:
*
* Legacy Widget + WidgetAdapterMixin
* |
* ComponentWrapper (Owl component)
* |
* Owl Component
*
* In this case, the parent legacy widget must use the WidgetAdapterMixin,
* which ensures that Owl hooks (mounted, willUnmount, destroy...) are
* properly called on the sub components. Moreover, it must instantiate a
* ComponentWrapper, and provide it the Owl component class to use alongside
* its props. This wrapper will ensure that the Owl component will be
* correctly updated (with willUpdateProps) like it would be if it was embed
* in an Owl hierarchy. Moreover, this wrapper automatically redirects all
* events triggered by the Owl component (or its descendants) to legacy
* custom events (trigger_up) on the parent legacy widget.
* For example:
* class MyComponent extends Component {}
* MyComponent.template = xml`<div>Owl component with value <t t-esc="props.value"/></div>`;
* const MyWidget = Widget.extend(WidgetAdapterMixin, {
* start() {
* this.component = new ComponentWrapper(this, MyComponent, {value: 44});
* return this.component.mount(this.el);
* },
* update() {
* return this.component.update({value: 45});
* },
* });
*/
export const WidgetAdapterMixin = {
/**
* Calls on_attach_callback on each child ComponentWrapper, which will
* call __callMounted on each sub component (recursively), to mark them
* as mounted.
*/
on_attach_callback() {
for (const component of children.get(this) || []) {
component.on_attach_callback();
}
},
/**
* Calls on_detach_callback on each child ComponentWrapper, which will
* call __callWillUnmount to mark itself and its children as no longer
* mounted.
*/
on_detach_callback() {
for (const component of children.get(this) || []) {
component.on_detach_callback();
}
},
/**
* Destroys each sub component when the widget is destroyed. We call the
* private __destroy function as there is no need to remove the el from
* the DOM (will be removed alongside this widget).
*/
destroy() {
for (const wrapper of children.get(this) || []) {
wrapper.destroy();
}
children.delete(this);
},
};
//----------------------------------//
// Low-level coordination functions //
//----------------------------------//
/**
* Calls "callback" recursively on a ComponentNode's hierarchy
*
* @param {ComponentNode}
* @param {Boolean} childrenFirst whether to execute on the bottom-most child first
* @param {Function} callback
*/
function recursiveCall(node, childrenFirst = false, callback) {
if (!childrenFirst) {
callback(node);
}
for (const child of Object.values(node.children)) {
recursiveCall(child, childrenFirst, callback);
}
if (childrenFirst) {
callback(node);
}
}
/**
* Make the node able to distinguish between
* mounting in the DOM and mounting outside of if.
* @param {ComponentNode} node
*/
function prepareForFinish(node) {
const fiber = node.fiber;
const complete = fiber.complete;
fiber.complete = function () {
// if target is not in dom
// just trigger mounted hooks on the Proxy, not on any other node
if (!this.target.ownerDocument.contains(this.target)) {
this.mounted = [this];
// We skipped a bunch of mounted calls.
// Following calls to patched may crash because of this.
// (e.g. useEffect dependencies set in mounted and used in patched)
}
complete.call(this);
};
}
const nodesToRemount = new WeakMap();
/**
* Pushed a node into the nodesToRemount WeakMap.
* The value is a callback that sets the node for remounting
* @param {ComponentNode} node
* @param {function} updateAndRender The original node's prototype's
*/
function setToRemount(node, updateAndRender) {
let toRemount = true;
if (!node.isPatched) {
node.isPatched = true;
node.mounted.push(() => {
toRemount = false;
});
node.willUpdateProps.push(() => {
const rootMounted = node.fiber.root.mounted;
if (toRemount && !rootMounted.includes(node.fiber)) {
rootMounted.push(node.fiber);
}
});
}
return () => toRemount = true;
}
/**
* Make the node able to remount its children nodes correctly
* Typically, in that case we don't call willPatch and patched hooks,
* rather, we want to call the mounted hooks
* @param {ComponentNode} mainNode
*/
function prepareForRemount(mainNode) {
const updateAndRender = mainNode.updateAndRender;
recursiveCall(mainNode, false, (node) => {
if (mainNode === node) {
return;
}
if (nodesToRemount.has(node)) {
nodesToRemount.get(node)();
return;
} else {
nodesToRemount.set(node, setToRemount(node, updateAndRender));
}
});
}
/**
* Registers a wrapper instance as a child of the given parent in the
* 'children' weakMap.
*
* @private
* @param {Widget} parent
*/
function registerWrapper(parent, wrapper) {
let parentChildren = children.get(parent);
if (!parentChildren) {
parentChildren = [];
children.set(parent, parentChildren);
}
parentChildren.push(wrapper);
}
/**
* The component class that will be instanciated between a legacy and an OWL 2 layer.
*/
class ProxyComponent extends LegacyComponent {
setup() {
for (const [hookName, cb] of Object.entries(this.props.hooks)) {
owl[hookName](cb);
}
onWillUnmount(() => {
// The current el will be change if we remount after unmounting
this._handledEvents = new Set();
});
this.parentWidget = this.props.parentWidget;
this._handledEvents = new Set();
useSubEnv({
[widgetSymbol]: this._addListener.bind(this)
});
}
/**
* Adds an event handler that will redirect the given Owl event to an
* Odoo legacy event. This function is called just before the event is
* actually triggered.
*
* @private
* @param {string} evType
*/
_addListener(evType) {
if (this.parentWidget && !this._handledEvents.has(evType) && status(this) === "mounted") {
this._handledEvents.add(evType);
this.el.addEventListener(evType, ev => {
// as the WrappeComponent has the same root node as the
// actual sub Component, we have to check that the event
// hasn't been stopped by that component (it would naturally
// call stopPropagation, whereas it should actually call
// stopImmediatePropagation to prevent from getting here)
if (!ev.cancelBubble) {
ev.stopPropagation();
const detail = Object.assign({}, ev.detail, {
__originalComponent: ev.originalComponent,
});
this.parentWidget.trigger_up(ev.type.replace(/-/g, '_'), detail);
}
});
}
}
}
ProxyComponent.template = xml`<t t-component="props.Component" t-props="props.props"/>`;
export class ComponentWrapper {
constructor(parent, Component, props) {
if (parent instanceof Component) {
throw new Error("ComponentWrapper must be used with a legacy Widget as parent");
}
this.setParent(parent);
const _env = props.env;
delete props.env;
this.props = props;
this.Component = Component;
const env = _env || owl.Component.env;
const appConfig = {
env,
templates,
dev: env.debug,
translatableAttributes: ["data-tooltip"],
translateFn: _t,
};
const app = new App(null, appConfig);
this.app = app;
this.node = this._makeOwlNode();
this.env = this.node.component.env;
app.root = this.node;
this.__owl__ = Object.create(this.node);
this.componentRef = { comp: null };
this.status = "new";
this.setup();
}
_makeOwlNode() {
const resolveRender = () => {
if (this.renderResolve) {
this.renderResolve();
}
};
const props = {
props: this.props,
hooks: {
onMounted: resolveRender,
onPatched: resolveRender,
onRendered: () => {
this.componentRef.comp = Object.values(this.node.children)[0].component;
},
},
Component: this.Component,
parentWidget: this.parentWidget,
};
return this.app.makeNode(ProxyComponent, props);
}
setup() { }
get el() {
return this.node.component.el;
}
//------------------//
// OWL 1 - like API //
//------------------//
async mount(target, options) {
if (this.status === "mounted" || this.status === "willMount") {
return this.render();
} else if (this.status === "destroyed") {
return;
}
if (target) {
this.target = target;
}
if (options) {
this.mountOptions = options;
}
const remount = this.status === "unmounted";
if (remount) {
prepareForRemount(this.node);
}
this.status = "willMount";
const prom = this.app.mountNode(this.node, this.target, this.mountOptions);
if (remount) {
this.node.fiber.deep = true;
}
prepareForFinish(this.node);
await prom;
if (this.target.ownerDocument.contains(this.target)) {
this.status = "mounted";
}
this.node.willStart = [];
// remove the promise.resolve from mounted callbacks
const mounted = this.node.mounted;
this.node.mounted = mounted.slice(0, mounted.length - 1);
return this;
}
unmount() {
this.on_detach_callback();
this.el.remove();
this.node.bdom = null;
}
render() {
if (this.status !== "mounted") {
return;
}
if (this.renderProm) {
this.node.render(true);
return this.renderProm;
}
this.renderProm = new Promise((resolve, reject) => {
this.renderResolve = resolve;
this.renderReject = reject;
}).then(() => {
this.renderProm = null;
this.renderResolve = null;
this.renderReject = null;
});
this.node.render(true);
return this.renderProm;
}
trigger() {
return this.node.component.trigger(...arguments);
}
destroy() {
if (["willMount", "unmounted"].includes(this.status)) {
recursiveCall(this.node, false, (node) => {
node.willUnmount = [];
});
}
this.app.destroy();
this.status = "destroyed";
}
//---------------------//
// API from legacy POV //
//---------------------//
get $el() {
return $(this.el);
}
on_attach_callback() {
if (!this.el || !this.el.ownerDocument.contains(this.el)) {
return;
}
if (this.status === "mounted") {
return;
}
recursiveCall(this.node, true, (node) => {
for (const cb of node.mounted) {
cb();
}
});
this.node.status = 1;
this.status = "mounted";
}
/**
* Calls willUnmount to notify the component it will be unmounted.
*/
on_detach_callback() {
if (this.status === "unmounted") {
return;
}
recursiveCall(this.node, false, (node) => {
// node.status might be "new" (0) here, if the component is
// currently being re-rendered, and a new component has just
// been instantiated, but as the rendering isn't completed, it
// isn't mounted yet
if (node.status === 1) {
const component = node.component;
for (const cb of node.willUnmount) {
cb.call(component);
}
}
});
this.node.status = 0;
this.status = "unmounted";
}
async update(nextProps) {
if (this.status === "destroyed") {
return;
}
const props = this.node.component.props.props;
const nextComponentProps = Object.assign({}, props, nextProps);
this.node.component.props.props = nextComponentProps;
this.props = nextComponentProps;
if (this.status === "unmounted") {
return this.mount(this.target);
} else {
return this.render();
}
}
setParent(parent) {
if (parent instanceof Component) {
throw new Error('ComponentWrapper must be used with a legacy Widget as parent');
}
if (parent) {
registerWrapper(parent, this);
}
if (this.parentWidget) {
const parentChildren = children.get(this.parentWidget);
parentChildren.splice(parentChildren.indexOf(this), 1);
}
this.parentWidget = parent;
if (this.node) {
this.node.component.parentWidget = parent;
}
}
}
export default {
ComponentAdapter,
ComponentWrapper,
WidgetAdapterMixin,
standaloneAdapter,
};
@@ -13,8 +13,8 @@ import {
makeLegacyDialogMappingService,
mapLegacyEnvToWowlEnv,
makeLegacyRPCService,
createWidgetParent,
} from "../../utils";
import { standaloneAdapter } from "@web/legacy/js/owl_compatibility";
import { makeEnv, startServices } from "@web/env";
import { loadJS, templates } from '@web/core/assets';
@@ -24,7 +24,7 @@ import { jsonrpc } from '@web/core/network/rpc_service';
import { renderToString } from "@web/core/utils/render";
import { _t } from "@web/core/l10n/translation";
import { omit } from "@web/core/utils/objects";
import { Component, App, whenReady } from "@odoo/owl";
import { App, whenReady } from "@odoo/owl";
import { getOrigin } from '@web/core/utils/urls';
const { Settings } = luxon;
@@ -395,8 +395,7 @@ export async function createPublicRoot(RootWidget) {
await startServices(wowlEnv);
mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv);
// The root widget's parent is a standalone adapter so that it has _trigger_up
const publicRoot = new RootWidget(standaloneAdapter({ Component }));
const publicRoot = new RootWidget(createWidgetParent(legacyEnv));
const app = new App(MainComponentsContainer, {
templates,
env: wowlEnv,
@@ -1,95 +0,0 @@
/** @odoo-module **/
/**
* @deprecated
* This component SHOULD NOT be used!
* It's used in legacy when components should work like in owl 1.
*
* If you need to get the component's `el` then use `useRef`
* and if you need to send info to the component's parent then use props.
*
* e.g.
* // OWL 1
* class Component_OWL1 extends owl.Component {
* static template = owl.tags.xml`
* <button t-on-click="() => this.trigger('up')">click me</button>
* `;
* mounted() {
* this.el.style = "color: red";
* }
* }
*
* // OWL 2
* class Component_OWL2 extends owl.Component {
* static template = owl.xml`
* <button t-ref="btn" t-on-click="() => props.up()">click me</button>
* `;
* setup() {
* this.btnRef = useRef("btn");
* owl.onMounted(() => {
* this.btnRef.el.style = "color: red";
* });
* }
* }
*/
export class LegacyComponent extends owl.Component {
get el() {
const bdom = this.__owl__.bdom;
if (!bdom) {
return null;
}
const el = (bdom.component && bdom.component.el) || bdom.firstNode();
return el.nodeType === Node.ELEMENT_NODE ? el : null;
}
/**
* Add a new method to owl Components to ensure that no performed RPC is
* resolved/rejected when the component is destroyed.
*/
rpc() {
return new Promise((resolve, reject) => {
return this.env.services
.rpc(...arguments)
.then((result) => {
if (owl.status(this) !== "destroyed") {
resolve(result);
}
})
.catch((reason) => {
if (owl.status(this) !== "destroyed") {
reject(reason);
}
});
});
}
/**
* Emit a custom event of type 'eventType' with the given 'payload' on the
* component's el, if it exists. However, note that the event will only bubble
* up to the parent DOM nodes. Thus, it must be called between mounted() and
* willUnmount().
*/
trigger(eventType, payload) {
this.__trigger(eventType, payload);
}
/**
* Private trigger method, allows to choose the component which triggered
* the event in the first place
*/
__trigger(eventType, payload) {
if (this.env[odoo.widgetSymbol]) {
this.env[odoo.widgetSymbol](eventType);
}
if (this.el) {
const ev = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: payload,
});
this.el.dispatchEvent(ev);
}
}
}
LegacyComponent.props = {
"*": true,
};
+93 -2
View File
@@ -2,8 +2,20 @@
import { browser } from "../core/browser/browser";
import { useService } from "@web/core/utils/hooks";
import { Component, useComponent, xml } from "@odoo/owl";
import { loadJS } from "@web/core/assets";
import {
App,
Component,
onMounted,
onWillStart,
onWillUnmount,
useComponent,
useEnv,
useRef,
useState,
xml,
} from "@odoo/owl";
import { loadJS, templates } from "@web/core/assets";
import { _t } from "@web/core/l10n/translation";
import {
ConnectionAbortedError,
ConnectionLostError,
@@ -234,3 +246,82 @@ export function useWowlService(serviceName) {
component.env = env;
return service;
}
export function createWidgetParent(env) {
return {
env,
_trigger_up: (ev) => {
if (ev.name === "call_service") {
let args = ev.data.args || [];
if (ev.data.service === "ajax" && ev.data.method === "rpc") {
// ajax service uses an extra 'target' argument for rpc
args = args.concat(ev.target);
}
const service = env.services[ev.data.service];
const result = service[ev.data.method].apply(service, args);
ev.data.callback(result);
}
},
};
}
export function useWidget(refName, widgetClass, params = []) {
const ref = useRef(refName);
const env = useEnv();
const parent = createWidgetParent(env);
const widget = new widgetClass(parent, ...params);
onWillStart(() => {
return widget._widgetRenderAndInsert(() => {});
});
onMounted(() => {
ref.el.append(widget.el);
});
onWillUnmount(() => {
widget.destroy();
});
return widget;
}
const rootTemplate = xml`<SubComp t-props="state"/>`;
export async function attachComponent(parent, element, componentClass, props = {}) {
class Root extends Component {
static template = rootTemplate;
static components = { SubComp: componentClass };
state = useState(props);
}
const env = Component.env;
const app = new App(Root, {
env,
templates,
dev: env.debug,
translatableAttributes: ["data-tooltip"],
translateFn: _t,
});
if (parent.__parentedMixin) {
parent.__parentedChildren.push({
get $el() {
return $(app.root.el);
},
destroy() {
app.destroy();
},
});
}
const component = await app.mount(element);
const subComp = Object.values(component.__owl__.children)[0].component;
return {
component: subComp,
destroy() {
app.destroy();
},
update(props) {
Object.assign(component.state, props);
},
};
}
@@ -1,48 +0,0 @@
/** @odoo-module **/
import makeTestEnvironment from "@web/../tests/legacy/helpers/test_env";
import testUtils from "@web/../tests/legacy/helpers/test_utils";
import { destroy, getFixture, mount } from "@web/../tests/helpers/utils";
import { LegacyComponent } from "@web/legacy/legacy_component";
const { xml } = owl;
let target;
QUnit.module("web", { beforeEach() { target = getFixture(); }}, function () {
QUnit.module("Component Extension");
QUnit.test("Component destroyed while performing successful RPC", async function (assert) {
assert.expect(1);
class Parent extends LegacyComponent {}
Parent.template = xml`<div/>`;
const env = makeTestEnvironment({}, () => Promise.resolve());
const parent = await mount(Parent, target, { env });
parent.rpc({}).then(() => { throw new Error(); });
destroy(parent);
await testUtils.nextTick();
assert.ok(true, "Promise should still be pending");
});
QUnit.test("Component destroyed while performing failed RPC", async function (assert) {
assert.expect(1);
class Parent extends LegacyComponent {}
Parent.template = xml`<div/>`;
const env = makeTestEnvironment({}, () => Promise.reject());
const parent = await mount(Parent, target, { env });
parent.rpc({}).catch(() => { throw new Error(); });
destroy(parent);
await testUtils.nextTick();
assert.ok(true, "Promise should still be pending");
});
});
File diff suppressed because it is too large Load Diff
-1
View File
@@ -58,7 +58,6 @@ Odoo Web Editor widget.
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
'web/static/src/libs/owl.js',
'web/static/src/legacy/legacy_component.js',
'web/static/lib/jquery/jquery.js',
'web/static/lib/jquery.ui/jquery-ui.js',
'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js',
@@ -17,7 +17,7 @@ import { debounce, throttleForAnimation } from "@web/core/utils/timing";
import { uniqueId } from "@web/core/utils/functions";
import { sortBy, unique } from "@web/core/utils/arrays";
import { browser } from "@web/core/browser/browser";
import { ComponentWrapper } from "@web/legacy/js/owl_compatibility";
import { attachComponent } from "@web/legacy/utils";
import { Toolbar } from "@web_editor/js/editor/toolbar";
import {
Component,
@@ -4297,14 +4297,13 @@ var SnippetsMenu = Widget.extend({
wysiwygState: Object,
};
}
const toolbarWrapper = new ComponentWrapper(this, WebsiteToolbar, {
wysiwygState: this.options.wysiwyg.state,
});
// Add the toolbarWrapperEl to the dom for owl to properly mount the
// Toolbar.
document.body.append(this._toolbarWrapperEl);
this._toolbarWrapperEl.style.display = 'none';
await toolbarWrapper.mount(this._toolbarWrapperEl);
await attachComponent(this, this._toolbarWrapperEl, WebsiteToolbar, {
wysiwygState: this.options.wysiwyg.state,
});
this._toolbarWrapperEl.style.display = 'contents';
const toolbarEl = this._toolbarWrapperEl.firstChild;
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { ComponentWrapper } from "@web/legacy/js/owl_compatibility";
import { attachComponent } from "@web/legacy/utils";
import { MediaDialog } from "@web_editor/components/media_dialog/media_dialog";
import Dialog from "@web/legacy/js/core/dialog";
import dom from "@web/legacy/js/core/dom";
@@ -1413,6 +1413,8 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({
const _super = this._super.bind(this);
const args = arguments;
this.resetTabCount = 0;
if (!this.options.dataAttributes.lazyPalette === 'true') {
await this._renderColorPalette();
}
@@ -1444,7 +1446,7 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({
this.colorPaletteWrapper?.update({
selectedCC: this._ccValue,
selectedColor: this._value,
resetTabCount: this.colorPaletteWrapper.node.component.props.props.resetTabCount + 1,
resetTabCount: ++this.resetTabCount,
});
} else {
// TODO review in master, this does async stuff. Maybe the open
@@ -1591,9 +1593,10 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({
* @private
* @returns {Promise}
*/
_renderColorPalette: function () {
_renderColorPalette: async function () {
this.resetTabCount = 0;
const options = {
resetTabCount: 0,
resetTabCount: this.resetTabCount,
selectedCC: this._ccValue,
selectedColor: this._value,
onSetColorNames: (colorNames) => {
@@ -1636,12 +1639,8 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({
options.document = this.$target[0].ownerDocument;
options.getTemplate = wysiwyg.getColorpickerTemplate.bind(wysiwyg);
}
if (this.colorPaletteWrapper) {
this.colorPaletteWrapper.destroy();
}
this.colorPaletteWrapper = new ComponentWrapper(this, ColorPalette, options);
return this.colorPaletteWrapper.mount(this.colorPaletteEl);
this.colorPaletteWrapper?.destroy();
this.colorPaletteWrapper = await attachComponent(this, this.colorPaletteEl, ColorPalette, options);
},
/**
* @override
@@ -6020,15 +6019,14 @@ registry.ImageTools = ImageHandlerOption.extend({
this.trigger_up('disable_loading_effect');
const img = this._getImg();
const document = this.$el[0].ownerDocument;
const imageCropWrapper = new ComponentWrapper(this, ImageCrop, {
const imageCropWrapperElement = document.createElement('div');
document.body.append(imageCropWrapperElement);
const imageCropWrapper = await attachComponent(this, imageCropWrapperElement, ImageCrop, {
rpc: this._rpc.bind(this),
activeOnStart: true,
media: img,
mimetype: this._getImageMimetype(img),
});
const imageCropWrapperElement = document.createElement('div');
document.body.append(imageCropWrapperElement);
await imageCropWrapper.mount(imageCropWrapperElement);
await new Promise(resolve => {
this.$target.one('image_cropper_destroyed', async () => {
@@ -6037,9 +6035,10 @@ registry.ImageTools = ImageHandlerOption.extend({
}
await this._reapplyCurrentShape();
resolve();
imageCropWrapperElement.remove();
});
});
imageCropWrapperElement.remove();
imageCropWrapper.destroy();
this.trigger_up('enable_loading_effect');
},
/**
@@ -6083,18 +6082,17 @@ registry.ImageTools = ImageHandlerOption.extend({
// Mount the ImageCrop to call the reset method. As we need the state of
// the component to be mounted before calling reset, mount it
// temporarily into the body.
const imageCropWrapper = new ComponentWrapper(this, ImageCrop, {
const imageCropWrapperElement = document.createElement('div');
document.body.append(imageCropWrapperElement);
const imageCropWrapper = await attachComponent(this, imageCropWrapperElement, ImageCrop, {
rpc: this._rpc.bind(this),
activeOnStart: true,
media: img,
mimetype: this._getImageMimetype(img),
});
const imageCropWrapperElement = document.createElement('div');
document.body.append(imageCropWrapperElement);
await imageCropWrapper.mount(imageCropWrapperElement);
await imageCropWrapper.componentRef.comp.mountedPromise;
await imageCropWrapper.componentRef.comp.reset();
imageCropWrapper.unmount();
await imageCropWrapper.component.mountedPromise;
await imageCropWrapper.component.reset();
imageCropWrapper.destroy();
imageCropWrapperElement.remove();
await this._reapplyCurrentShape();
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { loadBundle } from "@web/core/assets";
import { useWowlService } from '@web/legacy/utils';
import { useWowlService, attachComponent } from '@web/legacy/utils';
import { requireLegacyModule } from '@web_editor/js/frontend/loader';
export async function loadWysiwygFromTextarea(parent, textarea, options) {
@@ -16,7 +16,6 @@ export async function loadWysiwygFromTextarea(parent, textarea, options) {
currentOptions.value = '<p><br></p>';
}
const { ComponentWrapper } = await requireLegacyModule('@web/legacy/js/owl_compatibility')
const { Wysiwyg } = await requireLegacyModule('@web_editor/js/wysiwyg/wysiwyg', async () => {
await loadBundle("web_editor.assets_wysiwyg");
});
@@ -40,11 +39,10 @@ export async function loadWysiwygFromTextarea(parent, textarea, options) {
$form.append($textarea);
$wysiwygWrapper.html('');
const wysiwygWrapper = $wysiwygWrapper[0];
const componentWrapper = new ComponentWrapper(parent, LegacyWysiwyg, {
await attachComponent(parent, wysiwygWrapper, LegacyWysiwyg, {
options: currentOptions,
editingValue: currentOptions.value,
});
componentWrapper.mount(wysiwygWrapper);
$form.find('.note-editable').data('wysiwyg', wysiwyg);
@@ -1,10 +1,10 @@
/** @odoo-module */
import {ComponentAdapter} from '@web/legacy/js/owl_compatibility';
import { useWidget } from "@web/legacy/utils";
import {useService} from '@web/core/utils/hooks';
import AceEditor from '@web_editor/js/common/ace';
const {Component} = owl;
import { Component, xml } from "@odoo/owl";
export const WebsiteAceEditor = AceEditor.extend({
@@ -37,12 +37,7 @@ export const WebsiteAceEditor = AceEditor.extend({
// specific or because its parent was edited too and the view
// got copy/unlink).
const selectedView = Object.values(this.views).find(view => view.id === this._getSelectedResource());
let context;
this.trigger_up('context_get', {
callback: (ctx) => {
context = ctx;
},
});
const context = this.options.getContext();
defs.push(this._rpc({
model: 'ir.ui.view',
method: 'search_read',
@@ -97,37 +92,19 @@ export const WebsiteAceEditor = AceEditor.extend({
* @override
*/
_rpc(options) {
let context;
this.trigger_up('context_get', {
callback: (ctx) => {
context = ctx;
},
});
return this._super({...options, context: context});
return this._super({ ...options, context: this.options.getContext() });
},
});
export class AceEditorAdapterComponent extends ComponentAdapter {
setup() {
super.setup();
export class AceEditorAdapterComponent extends Component {
static template = xml`<div style="display: contents;" t-ref="wrapper"/>`;
setup() {
this.website = useService("website");
this.user = useService("user");
this.env = Component.env;
}
_trigger_up(event) {
if (event.name === 'context_get') {
return event.data.callback({...this.user.context, website_id: this.website.currentWebsite.id});
}
super._trigger_up(event);
}
/**
* @override
*/
get widgetArgs() {
return [
useWidget("wrapper", WebsiteAceEditor, [
this.website.pageDocument && this.website.pageDocument.documentElement.dataset.viewXmlid,
{
toggleAceEditor: () => this.website.context.showAceEditor = false,
@@ -139,10 +116,11 @@ export class AceEditorAdapterComponent extends ComponentAdapter {
reload: () => {
this.website.contentWindow.location.reload();
},
}
];
getContext: () => ({
...this.user.context,
website_id: this.website.currentWebsite.id,
}),
},
]);
}
}
AceEditorAdapterComponent.defaultProps = {
Component: WebsiteAceEditor,
};
-2
View File
@@ -157,8 +157,6 @@ Featuring
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
'web/static/src/libs/owl.js',
'web/static/src/legacy/js/component_extension.js',
'web/static/src/legacy/legacy_component.js',
'web/static/lib/jquery/jquery.js',
'web/static/lib/jquery.ui/jquery-ui.js',
'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js',