[FIX] web, point_of_sale: update owl to v1.2.4

This commit update owl from 1.2.3 to 1.2.4, and
introduces a few fixes for code that was broken (because it accesses
private owl values)

Note the fix for the test menu code is interesting. Before this commit,
the handler for the click on the home menu component was not called, so
the click had a side effect: it modified the url to its href, which
caused the action manager to load the next application, even though the
home menu was not mounted.

With the update to Owl, this is no longer true: the handler is called,
and call preventDefault on the event. Because of that, the url is not
changed. However, since the home menu is itself not mounted, it cannot
communicate to the web client that we should load the next app.  This is
why it was broken.  The fix is simple: we actually make sure that the
home menu is displayed before clicking on the app menuitem.

closes odoo/odoo#65906

X-original-commit: 2385e58d76aa5e4bca0fc6d76ecedc7de5fd4267
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
Signed-off-by: Géry Debongnie (ged) <ged@openerp.com>
This commit is contained in:
Géry Debongnie
2021-02-10 17:28:10 +00:00
parent 4ff5e072a1
commit addf9d7eec
6 changed files with 156 additions and 124 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ odoo.define('point_of_sale.Gui', function (require) {
get(target, key) {
const { component, availableMethods } = target;
if (!component) throw new Error(`Call 'configureGui' before using Gui.`);
const isMounted = component.__owl__.isMounted;
const isMounted = component.__owl__.status === 3 /* mounted */;
if (availableMethods.has(key) && isMounted) {
return component[key].bind(component);
}
+114 -95
View File
@@ -181,15 +181,15 @@
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
const WORD_REPLACEMENT = {
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<=",
};
const STATIC_TOKEN_MAP = {
});
const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {
"{": "LEFT_BRACE",
"}": "RIGHT_BRACE",
"[": "LEFT_BRACKET",
@@ -198,7 +198,7 @@
",": "COMMA",
"(": "LEFT_PAREN",
")": "RIGHT_PAREN",
};
});
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
@@ -2531,7 +2531,7 @@
code = ctx.captureExpression(value);
}
const modCode = mods.map((mod) => modcodes[mod]).join("");
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
let handler = `function (e) {if (context.__owl__.status === ${5 /* DESTROYED */}){return}${modCode}${code}}`;
if (putInCache) {
const key = ctx.generateTemplateKey(event);
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
@@ -3176,7 +3176,7 @@
// need to update component
let styleCode = "";
if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
styleCode = `.then(()=>{if (w${componentID}.__owl__.status === ${5 /* DESTROYED */}) {return};w${componentID}.el.style=${tattStyle};});`;
}
ctx.addLine(`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
@@ -3440,6 +3440,8 @@
*/
_reuseFiber(oldFiber) {
oldFiber.cancel(); // cancel children fibers
oldFiber.target = this.target || oldFiber.target;
oldFiber.position = this.position || oldFiber.position;
oldFiber.isCompleted = false; // keep the root fiber alive
oldFiber.isRendered = false; // the fiber has to be re-rendered
if (oldFiber.child) {
@@ -3519,8 +3521,8 @@
complete() {
let component = this.component;
this.isCompleted = true;
const { isMounted, isDestroyed } = component.__owl__;
if (isDestroyed) {
const status = component.__owl__.status;
if (status === 5 /* DESTROYED */) {
return;
}
// build patchQueue
@@ -3532,7 +3534,7 @@
this._walk(doWork);
const patchLen = patchQueue.length;
// call willPatch hook on each fiber of patchQueue
if (isMounted) {
if (status === 3 /* MOUNTED */) {
for (let i = 0; i < patchLen; i++) {
const fiber = patchQueue[i];
if (fiber.shouldPatch) {
@@ -3582,8 +3584,9 @@
component.__owl__.pvnode.elm = component.__owl__.vnode.elm;
}
}
if (fiber === component.__owl__.currentFiber) {
component.__owl__.currentFiber = null;
const compOwl = component.__owl__;
if (fiber === compOwl.currentFiber) {
compOwl.currentFiber = null;
}
}
// insert into the DOM (mount case)
@@ -3601,7 +3604,7 @@
this.component.env.qweb.trigger("dom-appended");
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
if (isMounted || inDOM) {
if (status === 3 /* MOUNTED */ || inDOM) {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
@@ -3616,6 +3619,13 @@
}
}
}
else {
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
component.__owl__.status = 4 /* UNMOUNTED */;
}
}
}
/**
* Cancel a fiber and all its children.
@@ -3845,6 +3855,15 @@
document.head.appendChild(sheet);
}
var STATUS;
(function (STATUS) {
STATUS[STATUS["CREATED"] = 0] = "CREATED";
STATUS[STATUS["WILLSTARTED"] = 1] = "WILLSTARTED";
STATUS[STATUS["RENDERED"] = 2] = "RENDERED";
STATUS[STATUS["MOUNTED"] = 3] = "MOUNTED";
STATUS[STATUS["UNMOUNTED"] = 4] = "UNMOUNTED";
STATUS[STATUS["DESTROYED"] = 5] = "DESTROYED";
})(STATUS || (STATUS = {}));
const portalSymbol = Symbol("portal"); // FIXME
//------------------------------------------------------------------------------
// Component
@@ -3892,16 +3911,18 @@
this.env.browser = browser;
}
this.env.qweb.on("update", this, () => {
if (this.__owl__.isMounted) {
this.render(true);
}
if (this.__owl__.isDestroyed) {
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
switch (this.__owl__.status) {
case 3 /* MOUNTED */:
this.render(true);
break;
case 5 /* DESTROYED */:
// this is unlikely to happen, but if a root widget is destroyed,
// we want to remove our subscription. The usual way to do that
// would be to perform some check in the destroy method, but since
// it is very performance sensitive, and since this is a rare event,
// we simply do it lazily
this.env.qweb.off("update", this);
break;
}
});
depth = 0;
@@ -3913,8 +3934,7 @@
depth: depth,
vnode: null,
pvnode: null,
isMounted: false,
isDestroyed: false,
status: 0 /* CREATED */,
parent: parent || null,
children: {},
cmap: {},
@@ -3936,6 +3956,7 @@
if (constr.style) {
this.__applyStyles(constr);
}
this.setup();
}
/**
* The `el` is the root element of the component. Note that it could be null:
@@ -3944,6 +3965,16 @@
get el() {
return this.__owl__.vnode ? this.__owl__.vnode.elm : null;
}
/**
* setup is run just after the component is constructed. This is the standard
* location where the component can setup its hooks. It has some advantages
* over the constructor:
* - it can be patched (useful in odoo ecosystem)
* - it does not need to propagate the arguments to the super call
*
* Note: this method should not be called manually.
*/
setup() { }
/**
* willStart is an asynchronous hook that can be implemented to perform some
* action before the initial rendering of a component.
@@ -4022,60 +4053,53 @@
* Note that a component can be mounted an unmounted several times
*/
async mount(target, options = {}) {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
if (position !== "self" && this.el.parentNode !== target) {
// in this situation, we are trying to mount a component on a different
// target. In this case, we need to unmount first, otherwise it will
// not work.
this.unmount();
}
else {
return Promise.resolve();
}
}
if (__owl__.isDestroyed) {
throw new Error("Cannot mount a destroyed component");
}
if (__owl__.currentFiber) {
const currentFiber = __owl__.currentFiber;
if (!currentFiber.target && !currentFiber.position) {
// this means we have a pending rendering, but it was a render operation,
// not a mount operation. We can simply update the fiber with the target
// and the position
currentFiber.target = target;
currentFiber.position = position;
return scheduler.addFiber(currentFiber);
}
else if (currentFiber.target === target && currentFiber.position === position) {
return scheduler.addFiber(currentFiber);
}
else {
scheduler.rejectFiber(currentFiber, "Mounting operation cancelled");
}
}
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => { });
const position = options.position || "last-child";
const __owl__ = this.__owl__;
const currentFiber = __owl__.currentFiber;
switch (__owl__.status) {
case 0 /* CREATED */: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__prepareAndRender(fiber, () => { });
return scheduler.addFiber(fiber);
}
case 1 /* WILLSTARTED */:
case 2 /* RENDERED */:
currentFiber.target = target;
currentFiber.position = position;
return scheduler.addFiber(currentFiber);
case 4 /* UNMOUNTED */: {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
}
case 3 /* MOUNTED */: {
if (position !== "self" && this.el.parentNode !== target) {
const fiber = new Fiber(null, this, true, target, position);
fiber.shouldPatch = false;
this.__render(fiber);
return scheduler.addFiber(fiber);
}
else {
return Promise.resolve();
}
}
case 5 /* DESTROYED */:
throw new Error("Cannot mount a destroyed component");
}
else {
this.__render(fiber);
}
return scheduler.addFiber(fiber);
}
/**
* The unmount method is the opposite of the mount method. It is useful
* to call willUnmount calls and remove the component from the DOM.
*/
unmount() {
if (this.__owl__.isMounted) {
if (this.__owl__.status === 3 /* MOUNTED */) {
this.__callWillUnmount();
this.el.remove();
}
@@ -4101,11 +4125,11 @@
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
const isMounted = __owl__.isMounted;
const status = __owl__.status;
const fiber = new Fiber(null, this, force, null, null);
Promise.resolve().then(() => {
if (__owl__.isMounted || !isMounted) {
if (fiber.isCompleted) {
if (__owl__.status === 3 /* MOUNTED */ || status !== 3 /* MOUNTED */) {
if (fiber.isCompleted || fiber.isRendered) {
return;
}
this.__render(fiber);
@@ -4131,7 +4155,7 @@
*/
destroy() {
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
if (__owl__.status !== 5 /* DESTROYED */) {
const el = this.el;
this.__destroy(__owl__.parent);
if (el) {
@@ -4172,13 +4196,12 @@
*/
__destroy(parent) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
if (__owl__.status === 3 /* MOUNTED */) {
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
__owl__.status = 4 /* UNMOUNTED */;
}
const children = __owl__.children;
for (let key in children) {
@@ -4189,7 +4212,7 @@
delete parent.__owl__.children[id];
__owl__.parent = null;
}
__owl__.isDestroyed = true;
__owl__.status = 5 /* DESTROYED */;
delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
@@ -4197,7 +4220,7 @@
}
__callMounted() {
const __owl__ = this.__owl__;
__owl__.isMounted = true;
__owl__.status = 3 /* MOUNTED */;
__owl__.currentFiber = null;
this.mounted();
if (__owl__.mountedCB) {
@@ -4210,7 +4233,7 @@
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
__owl__.status = 4 /* UNMOUNTED */;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCompleted = true;
__owl__.currentFiber.root.counter = 0;
@@ -4218,7 +4241,7 @@
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
if (comp.__owl__.status === 3 /* MOUNTED */) {
comp.__callWillUnmount();
}
}
@@ -4338,17 +4361,23 @@
}
async __prepareAndRender(fiber, cb) {
try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
const proms = Promise.all([
this.willStart(),
this.__owl__.willStartCB && this.__owl__.willStartCB(),
]);
this.__owl__.status = 1 /* WILLSTARTED */;
await proms;
if (this.__owl__.status === 5 /* DESTROYED */) {
return Promise.resolve();
}
}
catch (e) {
fiber.handleError(e);
return Promise.resolve();
}
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCompleted) {
this.__render(fiber);
this.__owl__.status = 2 /* RENDERED */;
cb();
}
}
@@ -4370,7 +4399,7 @@
for (let childKey in __owl__.children) {
const child = __owl__.children[childKey];
const childOwl = child.__owl__;
if (!childOwl.isMounted && childOwl.parentLastFiberId < fiber.id) {
if (childOwl.status !== 3 /* MOUNTED */ && childOwl.parentLastFiberId < fiber.id) {
// we only do here a "soft" destroy, meaning that we leave the child
// dom node alone, without removing it. Most of the time, it does not
// matter, because the child component is already unmounted. However,
@@ -4416,16 +4445,6 @@
fiber.handleError(error);
}
}
/**
* Only called by qweb t-component directive (when t-keepalive is set)
*/
__remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
/**
* Apply default props (only top level).
*
@@ -5372,9 +5391,9 @@
exports.utils = utils;
__info__.version = '1.2.3';
__info__.date = '2021-01-19T14:42:29.241Z';
__info__.hash = '490cf18';
__info__.version = '1.2.4';
__info__.date = '2021-02-10T13:24:25.187Z';
__info__.hash = '985e985';
__info__.url = 'https://github.com/odoo/owl';
@@ -13,12 +13,12 @@
return new Promise((resolve, reject) => {
return this.env.services.rpc(...arguments)
.then(result => {
if (!this.__owl__.isDestroyed) {
if (this.__owl__.status !== 5 /* not destroyed */) {
resolve(result);
}
})
.catch(reason => {
if (!this.__owl__.isDestroyed) {
if (this.__owl__.status !== 5) /* not destroyed */ {
reject(reason);
}
});
@@ -439,14 +439,14 @@ odoo.define('web.OwlCompatibility', function () {
* @return {Promise}
*/
async update(props = {}) {
if (this.__owl__.isDestroyed) {
if (this.__owl__.status === 5 /* destroyed */) {
return new Promise(() => {});
}
Object.assign(this.props, props);
let prom;
if (this.__owl__.isMounted) {
if (this.__owl__.status === 3 /* mounted */) {
prom = this.render();
} else {
// we may not be in the DOM, but actually want to be redrawn
+15 -2
View File
@@ -106,6 +106,9 @@
console.log("Testing app menu:", element.dataset.menuXmlid);
if (testedApps.indexOf(element.dataset.menuXmlid) >= 0) return; // Another infinite loop protection
testedApps.push(element.dataset.menuXmlid);
if (isEnterprise) {
await ensureHomeMenu();
}
await testMenuItem(element);
if (appsMenusOnly === true) return;
const subMenuItems = document.querySelectorAll('.o_menu_entry_lvl_1, .o_menu_entry_lvl_2, .o_menu_entry_lvl_3, .o_menu_entry_lvl_4');
@@ -113,8 +116,7 @@
await testMenuItem(subMenuItem);
}
if (isEnterprise) {
const homeMenu = document.querySelector('nav.o_main_navbar > a.o_menu_toggle.fa-th');
return triggerClick(homeMenu, "home menu toggle button");
await ensureHomeMenu();
}
}
@@ -276,6 +278,17 @@
return promise;
}
/**
* Make sure the home menu is open
*/
async function ensureHomeMenu() {
const menuToggle = document.querySelector('nav.o_main_navbar > a.o_menu_toggle.fa-th');
if (menuToggle) {
await triggerClick(menuToggle, 'home menu toggle button');
await waitForCondition(() => document.querySelector('.o_home_menu'));
}
}
const MOUSE_EVENTS = [
'mouseover',
'mouseenter',
@@ -12,6 +12,9 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
const { Component, tags, useState } = owl;
const { xml } = tags;
// from Owl internal status enum
const ISMOUNTED = 3;
const ISDESTROYED = 5;
const WidgetAdapter = Widget.extend(WidgetAdapterMixin, {
destroy() {
@@ -883,19 +886,19 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
await widget.appendTo(target);
assert.verifySteps(['init', 'willStart', 'mounted']);
assert.ok(component.__owl__.isMounted);
assert.ok(component.__owl__.status === ISMOUNTED);
widget.$el.detach();
widget.on_detach_callback();
assert.verifySteps(['willUnmount']);
assert.ok(!component.__owl__.isMounted);
assert.ok(component.__owl__.status !== ISMOUNTED);
widget.$el.appendTo(target);
widget.on_attach_callback();
assert.verifySteps(['mounted']);
assert.ok(component.__owl__.isMounted);
assert.ok(component.__owl__.status === ISMOUNTED);
widget.destroy();
@@ -903,7 +906,7 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
});
QUnit.test("isMounted with several sub components", async function (assert) {
assert.expect(11);
assert.expect(9);
let c1;
let c2;
@@ -922,31 +925,29 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
await widget.appendTo(target);
assert.strictEqual(widget.el.innerHTML, '<div>Component 1</div><div>Component 2</div>');
assert.ok(c1.__owl__.isMounted);
assert.ok(c2.__owl__.isMounted);
assert.ok(c1.__owl__.status === ISMOUNTED);
assert.ok(c2.__owl__.status === ISMOUNTED);
widget.$el.detach();
widget.on_detach_callback();
assert.ok(!c1.__owl__.isMounted);
assert.ok(!c2.__owl__.isMounted);
assert.ok(c1.__owl__.status !== ISMOUNTED);
assert.ok(c2.__owl__.status !== ISMOUNTED);
widget.$el.appendTo(target);
widget.on_attach_callback();
assert.ok(c1.__owl__.isMounted);
assert.ok(c2.__owl__.isMounted);
assert.ok(c1.__owl__.status === ISMOUNTED);
assert.ok(c2.__owl__.status === ISMOUNTED);
widget.destroy();
assert.ok(!c1.__owl__.isMounted);
assert.ok(!c2.__owl__.isMounted);
assert.ok(c1.__owl__.isDestroyed);
assert.ok(c2.__owl__.isDestroyed);
assert.ok(c1.__owl__.status === ISDESTROYED);
assert.ok(c2.__owl__.status === ISDESTROYED);
});
QUnit.test("isMounted with several levels of sub components", async function (assert) {
assert.expect(6);
assert.expect(5);
let child;
class MyChildComponent extends Component {
@@ -971,22 +972,21 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
await widget.appendTo(target);
assert.strictEqual(widget.el.innerHTML, '<div><div>child</div></div>');
assert.ok(child.__owl__.isMounted);
assert.ok(child.__owl__.status === ISMOUNTED);
widget.$el.detach();
widget.on_detach_callback();
assert.ok(!child.__owl__.isMounted);
assert.ok(child.__owl__.status !== ISMOUNTED);
widget.$el.appendTo(target);
widget.on_attach_callback();
assert.ok(child.__owl__.isMounted);
assert.ok(child.__owl__.status === ISMOUNTED);
widget.destroy();
assert.ok(!child.__owl__.isMounted);
assert.ok(child.__owl__.isDestroyed);
assert.ok(child.__owl__.status === ISDESTROYED);
});
QUnit.test("sub component can be updated (in DOM)", async function (assert) {
@@ -1041,14 +1041,14 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
widget.$el.detach();
widget.on_detach_callback();
assert.ok(!widget.component.__owl__.isMounted);
assert.ok(widget.component.__owl__.status !== ISMOUNTED);
await widget.update();
widget.$el.appendTo(target);
widget.on_attach_callback();
assert.ok(widget.component.__owl__.isMounted);
assert.ok(widget.component.__owl__.status === ISMOUNTED);
assert.strictEqual(widget.el.innerHTML, '<div>Component 2</div>');
widget.destroy();