diff --git a/addons/point_of_sale/static/src/js/Gui.js b/addons/point_of_sale/static/src/js/Gui.js index 700bacb8e8b..659f735a05e 100644 --- a/addons/point_of_sale/static/src/js/Gui.js +++ b/addons/point_of_sale/static/src/js/Gui.js @@ -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); } diff --git a/addons/web/static/lib/owl/owl.js b/addons/web/static/lib/owl/owl.js index d35e8e97484..c1cb28a4670 100644 --- a/addons/web/static/lib/owl/owl.js +++ b/addons/web/static/lib/owl/owl.js @@ -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'; diff --git a/addons/web/static/src/js/component_extension.js b/addons/web/static/src/js/component_extension.js index ce9a3228569..4edaee807dd 100644 --- a/addons/web/static/src/js/component_extension.js +++ b/addons/web/static/src/js/component_extension.js @@ -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); } }); diff --git a/addons/web/static/src/js/owl_compatibility.js b/addons/web/static/src/js/owl_compatibility.js index a33f5ebed58..b20ad7890f0 100644 --- a/addons/web/static/src/js/owl_compatibility.js +++ b/addons/web/static/src/js/owl_compatibility.js @@ -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 diff --git a/addons/web/static/src/js/tools/test_menus.js b/addons/web/static/src/js/tools/test_menus.js index 3b0523d8f65..9112b7f5961 100644 --- a/addons/web/static/src/js/tools/test_menus.js +++ b/addons/web/static/src/js/tools/test_menus.js @@ -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', diff --git a/addons/web/static/tests/owl_compatibility_tests.js b/addons/web/static/tests/owl_compatibility_tests.js index a4f453867cf..7be3b17b857 100644 --- a/addons/web/static/tests/owl_compatibility_tests.js +++ b/addons/web/static/tests/owl_compatibility_tests.js @@ -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, '
Component 1
Component 2
'); - 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, '
child
'); - 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, '
Component 2
'); widget.destroy();