From f96c88ee1a4392d52ff9e1d582bccc092ab7c721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 25 Feb 2022 08:32:46 +0000 Subject: [PATCH 1/5] [IMP] web: update owl to version 2.0.0 with fine grained reactivity Changelog : https://github.com/odoo/owl/blob/7845304b7e9cca465ec94af6b1b5272dadc70891/CHANGELOG.md Part-of: odoo/odoo#84314 --- addons/web/static/lib/owl/owl.js | 532 +++++++++++++++++-------------- 1 file changed, 284 insertions(+), 248 deletions(-) diff --git a/addons/web/static/lib/owl/owl.js b/addons/web/static/lib/owl/owl.js index e75015bef58..178c51bf68a 100644 --- a/addons/web/static/lib/owl/owl.js +++ b/addons/web/static/lib/owl/owl.js @@ -1510,7 +1510,9 @@ } function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) { key = key + "__slot_" + name; - const slots = (ctx.props && ctx.props.slots) || {}; + const nonReactiveProps = ctx.props && ctx.props[TARGET]; + const slots = nonReactiveProps ? nonReactiveProps.slots || {} : {}; + // const slots = (ctx.props && ctx.props.slots) || {}; const { __render, __ctx, __scope } = slots[name] || {}; const slotScope = Object.create(__ctx || {}); if (__scope) { @@ -1717,6 +1719,210 @@ return stopped; }; + // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object) + const TARGET = Symbol("Target"); + // Escape hatch to prevent reactivity system to turn something into a reactive + const SKIP = Symbol("Skip"); + // Special key to subscribe to, to be notified of key creation/deletion + const KEYCHANGES = Symbol("Key changes"); + const objectToString = Object.prototype.toString; + /** + * Checks whether a given value can be made into a reactive object. + * + * @param value the value to check + * @returns whether the value can be made reactive + */ + function canBeMadeReactive(value) { + if (typeof value !== "object") { + return false; + } + // extract "RawType" from strings like "[object RawType]" => this lets us + // ignore many native objects such as Promise (whose toString is [object Promise]) + // or Date ([object Date]). + const rawType = objectToString.call(value).slice(8, -1); + return rawType === "Object" || rawType === "Array"; + } + /** + * Mark an object or array so that it is ignored by the reactivity system + * + * @param value the value to mark + * @returns the object itself + */ + function markRaw(value) { + value[SKIP] = true; + return value; + } + /** + * Given a reactive objet, return the raw (non reactive) underlying object + * + * @param value a reactive value + * @returns the underlying value + */ + function toRaw(value) { + return value[TARGET] || value; + } + const targetToKeysToCallbacks = new WeakMap(); + /** + * Observes a given key on a target with an callback. The callback will be + * called when the given key changes on the target. + * + * @param target the target whose key should be observed + * @param key the key to observe (or Symbol(KEYCHANGES) for key creation + * or deletion) + * @param callback the function to call when the key changes + */ + function observeTargetKey(target, key, callback) { + if (!targetToKeysToCallbacks.get(target)) { + targetToKeysToCallbacks.set(target, new Map()); + } + const keyToCallbacks = targetToKeysToCallbacks.get(target); + if (!keyToCallbacks.get(key)) { + keyToCallbacks.set(key, new Set()); + } + keyToCallbacks.get(key).add(callback); + if (!callbacksToTargets.has(callback)) { + callbacksToTargets.set(callback, new Set()); + } + callbacksToTargets.get(callback).add(target); + } + /** + * Notify Reactives that are observing a given target that a key has changed on + * the target. + * + * @param target target whose Reactives should be notified that the target was + * changed. + * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created + * or deleted) + */ + function notifyReactives(target, key) { + const keyToCallbacks = targetToKeysToCallbacks.get(target); + if (!keyToCallbacks) { + return; + } + const callbacks = keyToCallbacks.get(key); + if (!callbacks) { + return; + } + // Loop on copy because clearReactivesForCallback will modify the set in place + for (const callback of [...callbacks]) { + clearReactivesForCallback(callback); + callback(); + } + } + const callbacksToTargets = new WeakMap(); + /** + * Clears all subscriptions of the Reactives associated with a given callback. + * + * @param callback the callback for which the reactives need to be cleared + */ + function clearReactivesForCallback(callback) { + const targetsToClear = callbacksToTargets.get(callback); + if (!targetsToClear) { + return; + } + for (const target of targetsToClear) { + const observedKeys = targetToKeysToCallbacks.get(target); + if (!observedKeys) { + continue; + } + for (const callbacks of observedKeys.values()) { + callbacks.delete(callback); + } + } + targetsToClear.clear(); + } + const reactiveCache = new WeakMap(); + /** + * Creates a reactive proxy for an object. Reading data on the reactive object + * subscribes to changes to the data. Writing data on the object will cause the + * notify callback to be called if there are suscriptions to that data. Nested + * objects and arrays are automatically made reactive as well. + * + * Whenever you are notified of a change, all subscriptions are cleared, and if + * you would like to be notified of any further changes, you should go read + * the underlying data again. We assume that if you don't go read it again after + * being notified, it means that you are no longer interested in that data. + * + * Subscriptions: + * + Reading a property on an object will subscribe you to changes in the value + * of that property. + * + Accessing an object keys (eg with Object.keys or with `for..in`) will + * subscribe you to the creation/deletion of keys. Checking the presence of a + * key on the object with 'in' has the same effect. + * - getOwnPropertyDescriptor does not currently subscribe you to the property. + * This is a choice that was made because changing a key's value will trigger + * this trap and we do not want to subscribe by writes. This also means that + * Object.hasOwnProperty doesn't subscribe as it goes through this trap. + * + * @param target the object for which to create a reactive proxy + * @param callback the function to call when an observed property of the + * reactive has changed + * @returns a proxy that tracks changes to it + */ + function reactive(target, callback = () => { }) { + if (!canBeMadeReactive(target)) { + throw new Error(`Cannot make the given value reactive`); + } + if (SKIP in target) { + return target; + } + const originalTarget = target[TARGET]; + if (originalTarget) { + return reactive(originalTarget, callback); + } + if (!reactiveCache.has(target)) { + reactiveCache.set(target, new Map()); + } + const reactivesForTarget = reactiveCache.get(target); + if (!reactivesForTarget.has(callback)) { + const proxy = new Proxy(target, { + get(target, key, proxy) { + if (key === TARGET) { + return target; + } + observeTargetKey(target, key, callback); + const value = Reflect.get(target, key, proxy); + if (!canBeMadeReactive(value)) { + return value; + } + return reactive(value, callback); + }, + set(target, key, value, proxy) { + const isNewKey = !Object.hasOwnProperty.call(target, key); + const originalValue = Reflect.get(target, key, proxy); + const ret = Reflect.set(target, key, value, proxy); + if (isNewKey) { + notifyReactives(target, KEYCHANGES); + } + // While Array length may trigger the set trap, it's not actually set by this + // method but is updated behind the scenes, and the trap is not called with the + // new value. We disable the "same-value-optimization" for it because of that. + if (originalValue !== value || (Array.isArray(target) && key === "length")) { + notifyReactives(target, key); + } + return ret; + }, + deleteProperty(target, key) { + const ret = Reflect.deleteProperty(target, key); + notifyReactives(target, KEYCHANGES); + notifyReactives(target, key); + return ret; + }, + ownKeys(target) { + observeTargetKey(target, KEYCHANGES, callback); + return Reflect.ownKeys(target); + }, + has(target, key) { + // TODO: this observes all key changes instead of only the presence of the argument key + observeTargetKey(target, KEYCHANGES, callback); + return Reflect.has(target, key); + }, + }); + reactivesForTarget.set(callback, proxy); + } + return reactivesForTarget.get(callback); + } + // Maps fibers to thrown errors const fibersInError = new WeakMap(); const nodeErrorHandlers = new WeakMap(); @@ -1787,9 +1993,8 @@ let current = node.fiber; if (current) { let root = current.root; - root.counter -= cancelFibers(current.children); + root.counter = root.counter + 1 - cancelFibers(current.children); current.children = []; - root.counter++; current.bdom = null; if (fibersInError.has(current)) { fibersInError.delete(current); @@ -1826,9 +2031,11 @@ this.bdom = null; this.children = []; this.appliedToDom = false; + this.deep = false; this.node = node; this.parent = parent; if (parent) { + this.deep = parent.deep; const root = parent.root; root.counter++; this.root = root; @@ -1871,7 +2078,7 @@ } current = undefined; // Step 2: patching the dom - node.patch(); + node._patch(); this.locked = false; // Step 4: calling all mounted lifecycle hooks let mountedFibers = this.mounted; @@ -1958,6 +2165,39 @@ function useComponent() { return currentNode.component; } + // ----------------------------------------------------------------------------- + // Integration with reactivity system (useState) + // ----------------------------------------------------------------------------- + const batchedRenderFunctions = new WeakMap(); + /** + * Creates a reactive object that will be observed by the current component. + * Reading data from the returned object (eg during rendering) will cause the + * component to subscribe to that data and be rerendered when it changes. + * + * @param state the state to observe + * @returns a reactive object that will cause the component to re-render on + * relevant changes + * @see reactive + */ + function useState(state) { + const node = getCurrent(); + let render = batchedRenderFunctions.get(node); + if (!render) { + render = batched(node.render.bind(node)); + batchedRenderFunctions.set(node, render); + // manual implementation of onWillDestroy to break cyclic dependency + node.willDestroy.push(clearReactivesForCallback.bind(null, render)); + } + return reactive(state, render); + } + function arePropsDifferent(props1, props2) { + for (let k in props1) { + if (props1[k] !== props2[k]) { + return true; + } + } + return Object.keys(props1).length !== Object.keys(props2).length; + } function component(name, props, key, ctx, parent) { let node = ctx.children[key]; let isDynamic = typeof name !== "string"; @@ -1975,7 +2215,10 @@ } const parentFiber = ctx.fiber; if (node) { - node.updateAndRender(props, parentFiber); + const currentProps = node.component.props[TARGET]; + if (parentFiber.deep || arePropsDifferent(currentProps, props)) { + node.updateAndRender(props, parentFiber); + } } else { // new component @@ -1991,8 +2234,7 @@ } node = new ComponentNode(C, props, ctx.app, ctx); ctx.children[key] = node; - const fiber = makeChildFiber(node, parentFiber); - node.initiateRender(fiber); + node.initiateRender(new Fiber(node, parentFiber)); } return node; } @@ -2017,6 +2259,7 @@ applyDefaultProps(props, C); const env = (parent && parent.childEnv) || app.env; this.childEnv = env; + props = useState(props); this.component = new C(props, env, this); this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this); this.component.setup(); @@ -2044,20 +2287,29 @@ this._render(fiber); } } - async render() { + async render(deep = false) { let current = this.fiber; if (current && current.root.locked) { await Promise.resolve(); // situation may have changed after the microtask tick current = this.fiber; } - if (current && !current.bdom && !fibersInError.has(current)) { - return; + if (current) { + if (!current.bdom && !fibersInError.has(current)) { + if (deep) { + // we want the render from this point on to be with deep=true + current.deep = deep; + } + return; + } + // if current rendering was with deep=true, we want this one to be the same + deep = deep || current.deep; } - if (!this.bdom && !current) { + else if (!this.bdom) { return; } const fiber = makeRootFiber(this); + fiber.deep = deep; this.fiber = fiber; this.app.scheduler.addFiber(fiber); await Promise.resolve(); @@ -2116,6 +2368,9 @@ this.fiber = fiber; const component = this.component; applyDefaultProps(props, component.constructor); + currentNode = this; + props = useState(props); + currentNode = null; const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props))); await prom; if (fiber !== this.fiber) { @@ -2175,6 +2430,14 @@ this.bdom.moveBefore(other ? other.bdom : null, afterNode); } patch() { + if (this.fiber && this.fiber.parent) { + // we only patch here renderings coming from above. renderings initiated + // by the component will be patched independently in the appropriate + // fiber.complete + this._patch(); + } + } + _patch() { const hasChildren = Object.keys(this.children).length > 0; this.bdom.patch(this.fiber.bdom, hasChildren); if (hasChildren) { @@ -3580,17 +3843,13 @@ slotDef = `{${slotStr.join(", ")}}`; } if (slotDef && !(ast.dynamicProps || hasSlotsProp)) { - props.push(`slots: ${slotDef}`); + this.helpers.add("markRaw"); + props.push(`slots: markRaw(${slotDef})`); } const propStr = `{${props.join(",")}}`; let propString = propStr; if (ast.dynamicProps) { - if (!props.length) { - propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)})`; - } - else { - propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`; - } + propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${props.length ? ", " + propStr : ""})`; } let propVar; if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) { @@ -3599,7 +3858,8 @@ propString = propVar; } if (slotDef && (ast.dynamicProps || hasSlotsProp)) { - this.addLine(`${propVar}.slots = Object.assign(${slotDef}, ${propVar}.slots)`); + this.helpers.add("markRaw"); + this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`); } // cmap key const key = this.generateComponentKey(); @@ -4534,8 +4794,8 @@ this.__owl__ = node; } setup() { } - render() { - this.__owl__.render(); + render(deep = false) { + this.__owl__.render(deep); } } Component.template = ""; @@ -4791,231 +5051,6 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration return true; } - // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object) - const TARGET = Symbol("Target"); - // Escape hatch to prevent reactivity system to turn something into a reactive - const SKIP = Symbol("Skip"); - // Special key to subscribe to, to be notified of key creation/deletion - const KEYCHANGES = Symbol("Key changes"); - const objectToString = Object.prototype.toString; - /** - * Checks whether a given value can be made into a reactive object. - * - * @param value the value to check - * @returns whether the value can be made reactive - */ - function canBeMadeReactive(value) { - if (typeof value !== "object") { - return false; - } - // extract "RawType" from strings like "[object RawType]" => this lets us - // ignore many native objects such as Promise (whose toString is [object Promise]) - // or Date ([object Date]). - const rawType = objectToString.call(value).slice(8, -1); - return rawType === "Object" || rawType === "Array"; - } - /** - * Mark an object or array so that it is ignored by the reactivity system - * - * @param value the value to mark - * @returns the object itself - */ - function markRaw(value) { - value[SKIP] = true; - return value; - } - /** - * Given a reactive objet, return the raw (non reactive) underlying object - * - * @param value a reactive value - * @returns the underlying value - */ - function toRaw(value) { - return value[TARGET] || value; - } - const targetToKeysToCallbacks = new WeakMap(); - /** - * Observes a given key on a target with an callback. The callback will be - * called when the given key changes on the target. - * - * @param target the target whose key should be observed - * @param key the key to observe (or Symbol(KEYCHANGES) for key creation - * or deletion) - * @param callback the function to call when the key changes - */ - function observeTargetKey(target, key, callback) { - if (!targetToKeysToCallbacks.get(target)) { - targetToKeysToCallbacks.set(target, new Map()); - } - const keyToCallbacks = targetToKeysToCallbacks.get(target); - if (!keyToCallbacks.get(key)) { - keyToCallbacks.set(key, new Set()); - } - keyToCallbacks.get(key).add(callback); - if (!callbacksToTargets.has(callback)) { - callbacksToTargets.set(callback, new Set()); - } - callbacksToTargets.get(callback).add(target); - } - /** - * Notify Reactives that are observing a given target that a key has changed on - * the target. - * - * @param target target whose Reactives should be notified that the target was - * changed. - * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created - * or deleted) - */ - function notifyReactives(target, key) { - const keyToCallbacks = targetToKeysToCallbacks.get(target); - if (!keyToCallbacks) { - return; - } - const callbacks = keyToCallbacks.get(key); - if (!callbacks) { - return; - } - // Loop on copy because clearReactivesForCallback will modify the set in place - for (const callback of [...callbacks]) { - clearReactivesForCallback(callback); - callback(); - } - } - const callbacksToTargets = new WeakMap(); - /** - * Clears all subscriptions of the Reactives associated with a given callback. - * - * @param callback the callback for which the reactives need to be cleared - */ - function clearReactivesForCallback(callback) { - const targetsToClear = callbacksToTargets.get(callback); - if (!targetsToClear) { - return; - } - for (const target of targetsToClear) { - const observedKeys = targetToKeysToCallbacks.get(target); - if (!observedKeys) { - continue; - } - for (const callbacks of observedKeys.values()) { - callbacks.delete(callback); - } - } - targetsToClear.clear(); - } - const reactiveCache = new WeakMap(); - /** - * Creates a reactive proxy for an object. Reading data on the reactive object - * subscribes to changes to the data. Writing data on the object will cause the - * notify callback to be called if there are suscriptions to that data. Nested - * objects and arrays are automatically made reactive as well. - * - * Whenever you are notified of a change, all subscriptions are cleared, and if - * you would like to be notified of any further changes, you should go read - * the underlying data again. We assume that if you don't go read it again after - * being notified, it means that you are no longer interested in that data. - * - * Subscriptions: - * + Reading a property on an object will subscribe you to changes in the value - * of that property. - * + Accessing an object keys (eg with Object.keys or with `for..in`) will - * subscribe you to the creation/deletion of keys. Checking the presence of a - * key on the object with 'in' has the same effect. - * - getOwnPropertyDescriptor does not currently subscribe you to the property. - * This is a choice that was made because changing a key's value will trigger - * this trap and we do not want to subscribe by writes. This also means that - * Object.hasOwnProperty doesn't subscribe as it goes through this trap. - * - * @param target the object for which to create a reactive proxy - * @param callback the function to call when an observed property of the - * reactive has changed - * @returns a proxy that tracks changes to it - */ - function reactive(target, callback = () => { }) { - if (!canBeMadeReactive(target)) { - throw new Error(`Cannot make the given value reactive`); - } - if (SKIP in target) { - return target; - } - const originalTarget = target[TARGET]; - if (originalTarget) { - return reactive(originalTarget, callback); - } - if (!reactiveCache.has(target)) { - reactiveCache.set(target, new Map()); - } - const reactivesForTarget = reactiveCache.get(target); - if (!reactivesForTarget.has(callback)) { - const proxy = new Proxy(target, { - get(target, key, proxy) { - if (key === TARGET) { - return target; - } - observeTargetKey(target, key, callback); - const value = Reflect.get(target, key, proxy); - if (!canBeMadeReactive(value)) { - return value; - } - return reactive(value, callback); - }, - set(target, key, value, proxy) { - const isNewKey = !Object.hasOwnProperty.call(target, key); - const originalValue = Reflect.get(target, key, proxy); - const ret = Reflect.set(target, key, value, proxy); - if (isNewKey) { - notifyReactives(target, KEYCHANGES); - } - // While Array length may trigger the set trap, it's not actually set by this - // method but is updated behind the scenes, and the trap is not called with the - // new value. We disable the "same-value-optimization" for it because of that. - if (originalValue !== value || (Array.isArray(target) && key === "length")) { - notifyReactives(target, key); - } - return ret; - }, - deleteProperty(target, key) { - const ret = Reflect.deleteProperty(target, key); - notifyReactives(target, KEYCHANGES); - notifyReactives(target, key); - return ret; - }, - ownKeys(target) { - observeTargetKey(target, KEYCHANGES, callback); - return Reflect.ownKeys(target); - }, - has(target, key) { - // TODO: this observes all key changes instead of only the presence of the argument key - observeTargetKey(target, KEYCHANGES, callback); - return Reflect.has(target, key); - }, - }); - reactivesForTarget.set(callback, proxy); - } - return reactivesForTarget.get(callback); - } - const batchedRenderFunctions = new WeakMap(); - /** - * Creates a reactive object that will be observed by the current component. - * Reading data from the returned object (eg during rendering) will cause the - * component to subscribe to that data and be rerendered when it changes. - * - * @param state the state to observe - * @returns a reactive object that will cause the component to re-render on - * relevant changes - * @see reactive - */ - function useState(state) { - const node = getCurrent(); - if (!batchedRenderFunctions.has(node)) { - batchedRenderFunctions.set(node, batched(() => node.render())); - onWillDestroy(() => clearReactivesForCallback(render)); - } - const render = batchedRenderFunctions.get(node); - const reactiveState = reactive(state, render); - return reactiveState; - } - // ----------------------------------------------------------------------------- // useRef // ----------------------------------------------------------------------------- @@ -5121,6 +5156,7 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration config.shouldNormalizeDom = false; config.mainEventHandler = mainEventHandler; UTILS.Portal = Portal; + UTILS.markRaw = markRaw; const blockDom = { config, // bdom entry points @@ -5176,8 +5212,8 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration __info__.version = '2.0.0-alpha.2'; - __info__.date = '2022-02-23T07:56:30.264Z'; - __info__.hash = '0dbd2bd'; + __info__.date = '2022-02-25T08:12:58.656Z'; + __info__.hash = '7845304'; __info__.url = 'https://github.com/odoo/owl'; From d90f481448a4a28e4bdfa7823295fdd1795616f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 25 Feb 2022 08:42:10 +0000 Subject: [PATCH 2/5] [REF] web, project: remove the component NotUpdatable With the new fine-grained reactivity in owl 2, this component is not necessary any more. Part-of: odoo/odoo#84314 --- .../src/project_sharing/project_sharing.js | 4 +-- .../src/project_sharing/project_sharing.xml | 4 +-- .../src/core/dialog/dialog_container.js | 12 +++---- .../src/core/main_components_container.js | 12 +++---- .../web/static/src/core/utils/components.js | 9 ------ .../web/static/src/webclient/navbar/navbar.js | 4 +-- .../static/src/webclient/navbar/navbar.xml | 8 ++--- addons/web/static/src/webclient/webclient.js | 2 -- addons/web/static/src/webclient/webclient.xml | 8 ++--- .../tests/core/utils/component_tests.js | 32 ++----------------- 10 files changed, 23 insertions(+), 72 deletions(-) diff --git a/addons/project/static/src/project_sharing/project_sharing.js b/addons/project/static/src/project_sharing/project_sharing.js index 98d4118cffd..c71d61bb93a 100644 --- a/addons/project/static/src/project_sharing/project_sharing.js +++ b/addons/project/static/src/project_sharing/project_sharing.js @@ -4,7 +4,7 @@ import { useBus, useService } from '@web/core/utils/hooks'; import { ActionContainer } from '@web/webclient/actions/action_container'; import { MainComponentsContainer } from "@web/core/main_components_container"; import { useOwnDebugContext } from "@web/core/debug/debug_context"; -import { ErrorHandler, NotUpdatable } from "@web/core/utils/components"; +import { ErrorHandler } from "@web/core/utils/components"; import { session } from '@web/session'; const { Component, useEffect } = owl; @@ -59,5 +59,5 @@ export class ProjectSharingWebClient extends Component { } } -ProjectSharingWebClient.components = { ActionContainer, ErrorHandler, NotUpdatable, MainComponentsContainer }; +ProjectSharingWebClient.components = { ActionContainer, ErrorHandler, MainComponentsContainer }; ProjectSharingWebClient.template = 'project.ProjectSharingWebClient'; diff --git a/addons/project/static/src/project_sharing/project_sharing.xml b/addons/project/static/src/project_sharing/project_sharing.xml index 3ebf4fe49ef..bff67f47ad2 100644 --- a/addons/project/static/src/project_sharing/project_sharing.xml +++ b/addons/project/static/src/project_sharing/project_sharing.xml @@ -2,9 +2,7 @@ - - - + diff --git a/addons/web/static/src/core/dialog/dialog_container.js b/addons/web/static/src/core/dialog/dialog_container.js index 19571f5abbf..cad66a043cc 100644 --- a/addons/web/static/src/core/dialog/dialog_container.js +++ b/addons/web/static/src/core/dialog/dialog_container.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { ErrorHandler, NotUpdatable } from "../utils/components"; +import { ErrorHandler } from "../utils/components"; const { Component, xml } = owl; @@ -22,18 +22,16 @@ export class DialogContainer extends Component { }); } } -DialogContainer.components = { ErrorHandler, NotUpdatable }; +DialogContainer.components = { ErrorHandler }; //Legacy : The div wrapping the t-foreach, is placed to avoid owl to delete non-owl dialogs. //This div can be removed after removing all legacy dialogs. DialogContainer.template = xml`
- - - - - + + +
diff --git a/addons/web/static/src/core/main_components_container.js b/addons/web/static/src/core/main_components_container.js index 6d23c22f224..5d24e51a6bc 100644 --- a/addons/web/static/src/core/main_components_container.js +++ b/addons/web/static/src/core/main_components_container.js @@ -1,6 +1,6 @@ /** @odoo-module */ import { registry } from "./registry"; -import { NotUpdatable, ErrorHandler } from "./utils/components"; +import { ErrorHandler } from "./utils/components"; const { Component, xml } = owl; @@ -27,12 +27,10 @@ export class MainComponentsContainer extends Component { MainComponentsContainer.template = xml`
- - - - - + + +
`; -MainComponentsContainer.components = { NotUpdatable, ErrorHandler }; +MainComponentsContainer.components = { ErrorHandler }; diff --git a/addons/web/static/src/core/utils/components.js b/addons/web/static/src/core/utils/components.js index 9d9915bfb99..7b8099325ba 100644 --- a/addons/web/static/src/core/utils/components.js +++ b/addons/web/static/src/core/utils/components.js @@ -2,15 +2,6 @@ const { Component, onError, useComponent, xml } = owl; -export class NotUpdatable extends Component { - setup() { - const node = useComponent().__owl__; - node.patch = () => {}; - node.updateAndRender = () => Promise.resolve(); - } -} -NotUpdatable.template = xml``; - export class ErrorHandler extends Component { setup() { onError((error) => { diff --git a/addons/web/static/src/webclient/navbar/navbar.js b/addons/web/static/src/webclient/navbar/navbar.js index f4af05e5267..93ebb3a713f 100644 --- a/addons/web/static/src/webclient/navbar/navbar.js +++ b/addons/web/static/src/webclient/navbar/navbar.js @@ -5,7 +5,7 @@ import { DropdownItem } from "@web/core/dropdown/dropdown_item"; import { useService, useBus } from "@web/core/utils/hooks"; import { registry } from "@web/core/registry"; import { debounce } from "@web/core/utils/timing"; -import { ErrorHandler, NotUpdatable } from "@web/core/utils/components"; +import { ErrorHandler } from "@web/core/utils/components"; const { Component, onWillDestroy, useExternalListener, useEffect, useRef } = owl; const systrayRegistry = registry.category("systray"); @@ -185,4 +185,4 @@ export class NavBar extends Component { } } NavBar.template = "web.NavBar"; -NavBar.components = { Dropdown, DropdownItem, MenuDropdown, NotUpdatable, ErrorHandler }; +NavBar.components = { Dropdown, DropdownItem, MenuDropdown, ErrorHandler }; diff --git a/addons/web/static/src/webclient/navbar/navbar.xml b/addons/web/static/src/webclient/navbar/navbar.xml index e7e738b5e3a..9f6a7cae96f 100644 --- a/addons/web/static/src/webclient/navbar/navbar.xml +++ b/addons/web/static/src/webclient/navbar/navbar.xml @@ -30,11 +30,9 @@ diff --git a/addons/web/static/src/webclient/webclient.js b/addons/web/static/src/webclient/webclient.js index 1f49110337d..56c0a76faf7 100644 --- a/addons/web/static/src/webclient/webclient.js +++ b/addons/web/static/src/webclient/webclient.js @@ -5,7 +5,6 @@ import { DebugMenu } from "@web/core/debug/debug_menu"; import { localization } from "@web/core/l10n/localization"; import { MainComponentsContainer } from "@web/core/main_components_container"; import { registry } from "@web/core/registry"; -import { NotUpdatable } from "@web/core/utils/components"; import { useBus, useService } from "@web/core/utils/hooks"; import { ActionContainer } from "./actions/action_container"; import { NavBar } from "./navbar/navbar"; @@ -112,7 +111,6 @@ export class WebClient extends Component { WebClient.components = { ActionContainer, NavBar, - NotUpdatable, MainComponentsContainer, }; WebClient.template = "web.WebClient"; diff --git a/addons/web/static/src/webclient/webclient.xml b/addons/web/static/src/webclient/webclient.xml index 4593ad40038..44d2f713dd0 100644 --- a/addons/web/static/src/webclient/webclient.xml +++ b/addons/web/static/src/webclient/webclient.xml @@ -2,12 +2,10 @@ - + - - - - + + diff --git a/addons/web/static/tests/core/utils/component_tests.js b/addons/web/static/tests/core/utils/component_tests.js index 4002a8df9c9..8e8bf39c225 100644 --- a/addons/web/static/tests/core/utils/component_tests.js +++ b/addons/web/static/tests/core/utils/component_tests.js @@ -1,42 +1,14 @@ /** @odoo-module **/ -import { NotUpdatable, ErrorHandler } from "@web/core/utils/components"; +import { ErrorHandler } from "@web/core/utils/components"; import { makeTestEnv } from "../../helpers/mock_env"; import { getFixture, mount } from "../../helpers/utils"; -const { Component, onMounted, onWillUpdateProps, xml } = owl; +const { Component, xml } = owl; QUnit.module("utils", () => { QUnit.module("components"); - QUnit.test("NotUpdatable component", async function (assert) { - class Child extends Component { - setup() { - onMounted(() => { - assert.step("mounted"); - }); - onWillUpdateProps(() => { - assert.step("willupdateprops"); - }); - } - } - Child.template = xml`
hey
`; - class Parent extends Component {} - Parent.template = xml` -
- - -
`; - Parent.components = { Child, NotUpdatable }; - - const target = getFixture(); - const parent = await mount(Parent, target, { env: makeTestEnv() }); - assert.verifySteps(["mounted", "mounted"]); - - await parent.render(); - assert.verifySteps(["willupdateprops"]); - }); - QUnit.test("ErrorHandler component", async function (assert) { class Boom extends Component {} Boom.template = xml`
`; From 09b4668c6bbd845e08b8f84cc9538a012c1bdf74 Mon Sep 17 00:00:00 2001 From: Jorge Pinna Puissant Date: Fri, 25 Feb 2022 08:46:16 +0000 Subject: [PATCH 3/5] [REF] *: adapt code to owl2 with fine-grained reactivity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On most of the cases, the fine-grained reactivity is bypassed by: - Setting the optional `deep` argument as `true` on the renderer function. This will force a render to update all child components. - Changing some objects from reactive to non-reactive using `toRaw` function. Part-of: odoo/odoo#84314 Co-authored-by: Géry Debongnie --- .../tests/base_automation_error_dialog.js | 8 ++++++-- .../widgets/form_renderer/form_renderer.js | 7 +++++++ .../static/src/entry/chrome_adapter.js | 2 +- addons/point_of_sale/static/src/js/Chrome.js | 8 +++++--- .../ClientListScreen/ClientDetailsEdit.js | 2 +- .../ClientListScreen/ClientListScreen.js | 14 +++++++------- .../js/Screens/PaymentScreen/PaymentScreen.js | 10 +++++----- .../Screens/ProductScreen/ProductsWidget.js | 2 +- .../legacy/js/control_panel/control_panel.js | 15 ++++++++++++--- .../control_panel_model_extension.js | 3 ++- .../src/legacy/js/control_panel/search_bar.js | 4 ++-- .../src/legacy/js/fields/field_wrapper.js | 4 ++-- .../static/src/legacy/js/owl_compatibility.js | 10 +++++++--- .../src/search/control_panel/control_panel.js | 5 ++++- .../search/control_panel/control_panel.xml | 5 +---- addons/web/static/src/search/pager_hook.js | 8 ++++++-- addons/web/static/src/search/search_model.js | 4 ++-- .../src/search/with_search/with_search.js | 4 ++-- addons/web/static/src/views/view.js | 4 ++-- .../web/static/tests/search/control_panel.js | 8 +++----- .../static/tests/search/pager_hook_tests.js | 19 ++++++++++++++++++- 21 files changed, 96 insertions(+), 50 deletions(-) diff --git a/addons/base_automation/static/tests/base_automation_error_dialog.js b/addons/base_automation/static/tests/base_automation_error_dialog.js index be88ab2071d..5dfb772a0a3 100644 --- a/addons/base_automation/static/tests/base_automation_error_dialog.js +++ b/addons/base_automation/static/tests/base_automation_error_dialog.js @@ -16,7 +16,7 @@ import { BaseAutomationErrorDialog } from "../src/js/base_automation_error_dialo import { patchWithCleanup,getFixture, mount, nextTick } from "@web/../tests/helpers/utils"; import { DialogContainer } from "@web/core/dialog/dialog_container"; -const { onMounted } = owl; +const { onMounted, toRaw } = owl; const serviceRegistry = registry.category("services"); @@ -72,7 +72,11 @@ QUnit.module("base_automation", {}, function () { patchWithCleanup(BaseAutomationErrorDialog.prototype, { setup() { - assert.equal(this.props.data.context, errorContext, "Received the correct error context"); + assert.equal( + toRaw(this.props.data.context), + errorContext, + "Received the correct error context" + ); this._super(); }, }); diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer.js b/addons/mail/static/src/widgets/form_renderer/form_renderer.js index 89868f8ee16..993640de1f5 100644 --- a/addons/mail/static/src/widgets/form_renderer/form_renderer.js +++ b/addons/mail/static/src/widgets/form_renderer/form_renderer.js @@ -155,6 +155,13 @@ FormRenderer.include({ * @override */ _updateView() { + /** + * The chatter is detached before it's removed on the super._updateView function, + * this is done to avoid losing the event handlers. + */ + if (this._hasChatter()) { + this._chatterContainerTarget.remove(); + } this._super(...arguments); if (this._hasChatter()) { this.$chatterContainerHook.replaceWith(this._chatterContainerTarget); diff --git a/addons/point_of_sale/static/src/entry/chrome_adapter.js b/addons/point_of_sale/static/src/entry/chrome_adapter.js index bc5853144e9..f31874fbeae 100644 --- a/addons/point_of_sale/static/src/entry/chrome_adapter.js +++ b/addons/point_of_sale/static/src/entry/chrome_adapter.js @@ -49,7 +49,7 @@ export class ChromeAdapter extends Component { const updateUI = debounce(() => { if (this.env.isMobile !== currentIsMobile) { currentIsMobile = this.env.isMobile; - this.render(); + this.render(true); } }, 15); useExternalListener(window, "resize", updateUI); diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js index 9eaa93f02fc..f86cc2baa1e 100644 --- a/addons/point_of_sale/static/src/js/Chrome.js +++ b/addons/point_of_sale/static/src/js/Chrome.js @@ -10,7 +10,7 @@ odoo.define('point_of_sale.Chrome', function(require) { const PopupControllerMixin = require('point_of_sale.PopupControllerMixin'); const Registries = require('point_of_sale.Registries'); const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen'); - const { identifyError } = require('point_of_sale.utils'); + const { identifyError, batched } = require('point_of_sale.utils'); const { odooExceptionTitleMap } = require("@web/core/errors/error_dialogs"); const { ConnectionLostError, ConnectionAbortedError, RPCError } = require('@web/core/network/rpc_service'); const { useBus } = require("@web/core/utils/hooks"); @@ -26,6 +26,7 @@ odoo.define('point_of_sale.Chrome', function(require) { useRef, useState, useSubEnv, + reactive, } = owl; /** @@ -74,7 +75,8 @@ odoo.define('point_of_sale.Chrome', function(require) { this.previous_touch_y_coordinate = -1; - useSubEnv({ pos: useState(this.env.pos) }); + const pos = reactive(this.env.pos, batched(() => this.render(true))) + useSubEnv({ pos }); onMounted(() => { // remove default webclient handlers that induce click delay @@ -200,7 +202,7 @@ odoo.define('point_of_sale.Chrome', function(require) { this.env.pos.loadPartnersBackground(); } if (this.env.pos.config.limited_products_loading && this.env.pos.config.product_load_background) { - this.env.pos.loadProductsBackground().then(() => this.render()); + this.env.pos.loadProductsBackground().then(() => this.render(true)); } } diff --git a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js index 89afc6be087..1eacd4404fc 100644 --- a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js +++ b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js @@ -78,7 +78,7 @@ odoo.define('point_of_sale.ClientDetailsEdit', function(require) { const resizedImage = await this._resizeImage(loadedImage, 800, 600); this.changes.image_1920 = resizedImage.toDataURL(); // Rerender to reflect the changes in the screen - this.render(); + this.render(true); } } } diff --git a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js index ae21fc4b1f7..0f64aac56ed 100644 --- a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js @@ -56,7 +56,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { back() { if(this.state.detailIsShown) { this.state.detailIsShown = false; - this.render(); + this.render(true); } else { this.props.resolve({ confirmed: false, payload: false }); this.trigger('close-temp-screen'); @@ -110,7 +110,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { this.state.selectedClient = clients[0]; this.clickNext(); } else { - this.render(); + this.render(true); } } clickClient(partner) { @@ -119,14 +119,14 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { } else { this.state.selectedClient = partner; } - this.render(); + this.render(true); } editClient() { this.state.editModeProps = { partner: this.state.selectedClient, }; this.state.detailIsShown = true; - this.render(); + this.render(true); } clickNext() { this.state.selectedClient = this.nextButton.command === 'set' ? this.state.selectedClient : null; @@ -142,7 +142,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { partner: this.state.selectedClient, }; } - this.render(); + this.render(true); } async saveChanges(event) { try { @@ -154,7 +154,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { await this.env.pos.load_new_partners(); this.state.selectedClient = this.env.pos.db.get_partner_by_id(partnerId); this.state.detailIsShown = false; - this.render(); + this.render(true); } catch (error) { if (isConnectionError(error)) { await this.showPopup('OfflineErrorPopup', { @@ -175,7 +175,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { body: this.env._t('No customer found'), }); } - this.render(); + this.render(true); } async getNewClient() { var domain = []; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index a33ef5d66a6..6f06c953e47 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -96,7 +96,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { toggleIsToInvoice() { // click_invoice this.currentOrder.set_to_invoice(!this.currentOrder.is_to_invoice()); - this.render(); + this.render(true); } openCashbox() { this.env.proxy.printer.open_cashbox(); @@ -120,7 +120,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { toggleIsToShip() { // click_ship this.currentOrder.set_to_ship(!this.currentOrder.is_to_ship()); - this.render(); + this.render(true); } deletePaymentLine(event) { var self = this; @@ -135,13 +135,13 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { line.payment_method.payment_terminal.send_payment_cancel(this.currentOrder, cid).then(function() { self.currentOrder.remove_paymentline(line); NumberBuffer.reset(); - self.render(); + self.render(true); }) } else if (line.get_payment_status() !== 'waitingCancel') { this.currentOrder.remove_paymentline(line); NumberBuffer.reset(); - this.render(); + this.render(true); } } selectPaymentLine(event) { @@ -149,7 +149,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { const line = this.paymentLines.find((line) => line.cid === cid); this.currentOrder.select_paymentline(line); NumberBuffer.reset(); - this.render(); + this.render(true); } async validateOrder(isForceValidate) { if(this.env.pos.config.cash_rounding) { diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js index 10f2cdb859d..bff9350d8d0 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js @@ -82,7 +82,7 @@ odoo.define('point_of_sale.ProductsWidget', function(require) { this.state.searchWord = ''; } _updateProductList(event) { - this.render(); + this.render(true); this.trigger('switch-category', 0); } } diff --git a/addons/web/static/src/legacy/js/control_panel/control_panel.js b/addons/web/static/src/legacy/js/control_panel/control_panel.js index 5b66c659d18..119f6074c63 100644 --- a/addons/web/static/src/legacy/js/control_panel/control_panel.js +++ b/addons/web/static/src/legacy/js/control_panel/control_panel.js @@ -11,7 +11,16 @@ odoo.define('web.ControlPanel', function (require) { const SearchBar = require('web.SearchBar'); const { useModel } = require('web.Model'); - const { Component, onMounted, onPatched, onWillDestroy, onWillUpdateProps, useRef, useSubEnv } = owl; + const { + Component, + onMounted, + onPatched, + onWillDestroy, + onWillUpdateProps, + toRaw, + useRef, + useSubEnv, + } = owl; /** * TODO: remove this whole mechanism as soon as `cp_content` is completely removed. @@ -122,7 +131,7 @@ odoo.define('web.ControlPanel', function (require) { searchViewButtons: useRef('searchViewButtons'), }; - this.fields = this._formatFields(this.props.fields); + this.fields = this._formatFields(toRaw(this.props.fields || {})); this.sprintf = _.str.sprintf; @@ -155,7 +164,7 @@ odoo.define('web.ControlPanel', function (require) { subEnvView = nextProps.view; } if ("fields" in nextProps) { - this.fields = this._formatFields(nextProps.fields); + this.fields = this._formatFields(toRaw(nextProps.fields)); } this.additionalContent = getAdditionalContent(nextProps); }); diff --git a/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js b/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js index b97b6f2fe65..7deedae7996 100644 --- a/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js +++ b/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js @@ -13,6 +13,7 @@ odoo.define("web/static/src/js/control_panel/control_panel_model_extension.js", const FAVORITE_SHARED_GROUP = 2; const DISABLE_FAVORITE = "search_disable_custom_filters"; + const { toRaw } = owl; /** * Control panel model * @@ -158,7 +159,7 @@ odoo.define("web/static/src/js/control_panel/control_panel_model_extension.js", this.actionContext = Object.assign({}, this.config.context); this.searchMenuTypes = this.config.searchMenuTypes || []; this.favoriteFilters = this.config.favoriteFilters || []; - this.fields = this.config.fields || {}; + this.fields = toRaw(this.config.fields || {}); this.searchDefaults = {}; for (const key in this.actionContext) { const match = /^search_default_(.*)$/.exec(key); diff --git a/addons/web/static/src/legacy/js/control_panel/search_bar.js b/addons/web/static/src/legacy/js/control_panel/search_bar.js index ac06f92852b..2f43b34b190 100644 --- a/addons/web/static/src/legacy/js/control_panel/search_bar.js +++ b/addons/web/static/src/legacy/js/control_panel/search_bar.js @@ -7,7 +7,7 @@ odoo.define('web.SearchBar', function (require) { const { useModel } = require('web.Model'); const { fuzzyTest } = require('@web/core/utils/search'); - const { Component, onMounted, onWillUnmount, useExternalListener, useState } = owl; + const { Component, onMounted, onWillUnmount, toRaw, useExternalListener, useState } = owl; const CHAR_FIELDS = ['char', 'html', 'many2many', 'many2one', 'one2many', 'text']; let sourceId = 0; @@ -91,7 +91,7 @@ odoo.define('web.SearchBar', function (require) { * @returns {Object} */ _createSource(filter) { - const field = this.props.fields[filter.fieldName]; + const field = toRaw(this.props.fields)[filter.fieldName]; const type = field.type === "reference" ? "char" : field.type; const source = { active: true, diff --git a/addons/web/static/src/legacy/js/fields/field_wrapper.js b/addons/web/static/src/legacy/js/fields/field_wrapper.js index 4f750ac516f..797bd8821b1 100644 --- a/addons/web/static/src/legacy/js/fields/field_wrapper.js +++ b/addons/web/static/src/legacy/js/fields/field_wrapper.js @@ -4,7 +4,7 @@ odoo.define('web.FieldWrapper', function (require) { const { ComponentWrapper } = require('web.OwlCompatibility'); const field_utils = require('web.field_utils'); - const { App } = owl; + const { App, reactive } = owl; /** * This file defines the FieldWrapper component, an extension of ComponentWrapper, @@ -178,7 +178,7 @@ odoo.define('web.FieldWrapper', function (require) { const fieldsInfo = this.props.record.fieldsInfo[viewType]; fieldsInfo[this.props.fieldName].modifiersValue = modifiers || {}; } - this.componentRef.comp.props = this.props; + this.componentRef.comp.props = reactive(this.props); } } diff --git a/addons/web/static/src/legacy/js/owl_compatibility.js b/addons/web/static/src/legacy/js/owl_compatibility.js index 4e4cefb411e..5288b691d70 100644 --- a/addons/web/static/src/legacy/js/owl_compatibility.js +++ b/addons/web/static/src/legacy/js/owl_compatibility.js @@ -596,11 +596,15 @@ odoo.define('web.OwlCompatibility', function (require) { if (options) { this.mountOptions = options; } - if (this.status === "unmounted") { + 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 (document.contains(this.target)) { @@ -621,7 +625,7 @@ odoo.define('web.OwlCompatibility', function (require) { render() { if (this.renderProm) { - this.node.render(); + this.node.render(true); return this.renderProm; } this.renderProm = new Promise((resolve, reject) => { @@ -632,7 +636,7 @@ odoo.define('web.OwlCompatibility', function (require) { this.renderResolve = null; this.renderReject = null; }); - this.node.render(); + this.node.render(true); return this.renderProm; } diff --git a/addons/web/static/src/search/control_panel/control_panel.js b/addons/web/static/src/search/control_panel/control_panel.js index 552146c5ba3..65ea3c45a4c 100644 --- a/addons/web/static/src/search/control_panel/control_panel.js +++ b/addons/web/static/src/search/control_panel/control_panel.js @@ -8,7 +8,7 @@ import { FilterMenu } from "../filter_menu/filter_menu"; import { GroupByMenu } from "../group_by_menu/group_by_menu"; import { SearchBar } from "../search_bar/search_bar"; -const { Component } = owl; +const { Component, useState } = owl; const MAPPING = { filter: FilterMenu, @@ -20,6 +20,9 @@ const MAPPING = { export class ControlPanel extends Component { setup() { this.actionService = useService("action"); + this.pagerProps = this.env.config.pagerProps + ? useState(this.env.config.pagerProps) + : undefined; } /** diff --git a/addons/web/static/src/search/control_panel/control_panel.xml b/addons/web/static/src/search/control_panel/control_panel.xml index ac8b1382c45..2882a20c2ce 100644 --- a/addons/web/static/src/search/control_panel/control_panel.xml +++ b/addons/web/static/src/search/control_panel/control_panel.xml @@ -29,10 +29,7 @@ diff --git a/addons/web/static/src/search/pager_hook.js b/addons/web/static/src/search/pager_hook.js index 8756d5aa2af..b0f25d8ff36 100644 --- a/addons/web/static/src/search/pager_hook.js +++ b/addons/web/static/src/search/pager_hook.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -const { useEnv, useChildSubEnv } = owl; +const { useEnv, useChildSubEnv, useState, onWillRender } = owl; /** * @typedef PagerUpdateParams @@ -23,11 +23,15 @@ const { useEnv, useChildSubEnv } = owl; */ export function usePager(getProps) { const env = useEnv(); + const pagerState = useState(getProps()); useChildSubEnv({ config: { ...env.config, - getPagerProps: getProps, + pagerProps: pagerState, }, }); + onWillRender(() => { + Object.assign(pagerState, getProps()); + }); } diff --git a/addons/web/static/src/search/search_model.js b/addons/web/static/src/search/search_model.js index 968043e1633..3d58a856eb4 100644 --- a/addons/web/static/src/search/search_model.js +++ b/addons/web/static/src/search/search_model.js @@ -17,7 +17,7 @@ import { } from "./utils/dates"; import { FACET_ICONS } from "./utils/misc"; -const { EventBus } = owl; +const { EventBus, toRaw } = owl; const { DateTime } = luxon; /** @@ -215,7 +215,7 @@ export class SearchModel extends EventBus { const { comparison, context, domain, groupBy, orderBy } = config; this.globalComparison = comparison; - this.globalContext = context || {}; + this.globalContext = toRaw(context || {}); this.globalDomain = domain || []; this.globalGroupBy = groupBy || []; this.globalOrderBy = orderBy || []; diff --git a/addons/web/static/src/search/with_search/with_search.js b/addons/web/static/src/search/with_search/with_search.js index 66a275dcfa1..60fb360eb54 100644 --- a/addons/web/static/src/search/with_search/with_search.js +++ b/addons/web/static/src/search/with_search/with_search.js @@ -4,7 +4,7 @@ import { useBus, useService } from "@web/core/utils/hooks"; import { SearchModel } from "@web/search/search_model"; import { CallbackRecorder, useSetupAction } from "@web/webclient/actions/action_hook"; -const { Component, onWillStart, onWillUpdateProps, useSubEnv, useChildSubEnv } = owl; +const { Component, onWillStart, onWillUpdateProps, toRaw, useSubEnv } = owl; export const SEARCH_KEYS = ["comparison", "context", "domain", "groupBy", "orderBy"]; const OTHER_SEARCH_KEYS = ["irFilters", "searchViewArch", "searchViewFields", "searchViewId"]; @@ -36,7 +36,7 @@ export class WithSearch extends Component { }); onWillStart(async () => { - const config = { ...this.props }; + const config = { ...toRaw(this.props) }; if (config.globalState && config.globalState.searchModel) { config.state = JSON.parse(config.globalState.searchModel); delete config.globalState; diff --git a/addons/web/static/src/views/view.js b/addons/web/static/src/views/view.js index 782a3b735f0..fd4efbd80c8 100644 --- a/addons/web/static/src/views/view.js +++ b/addons/web/static/src/views/view.js @@ -9,7 +9,7 @@ import { WithSearch } from "@web/search/with_search/with_search"; import { useActionLinks } from "@web/views/helpers/view_hook"; import { extractLayoutComponents } from "@web/views/layout"; -const { Component, onWillUpdateProps, onWillStart, useSubEnv } = owl; +const { Component, onWillUpdateProps, onWillStart, toRaw, useSubEnv } = owl; const viewRegistry = registry.category("views"); /** @typedef {Object} Config @@ -282,7 +282,7 @@ export class View extends Component { // prepare the WithSearch component props this.withSearchProps = { - ...this.props, + ...toRaw(this.props), Component: ViewClass, componentProps: viewProps, }; diff --git a/addons/web/static/tests/search/control_panel.js b/addons/web/static/tests/search/control_panel.js index 13a980646d7..2861d2bfeb8 100644 --- a/addons/web/static/tests/search/control_panel.js +++ b/addons/web/static/tests/search/control_panel.js @@ -107,7 +107,7 @@ QUnit.module("Search", (hooks) => { }); QUnit.test("pager", async (assert) => { - let pagerInfo = { + let pagerProps = { offset: 0, limit: 10, total: 50, @@ -119,15 +119,13 @@ QUnit.module("Search", (hooks) => { resModel: "foo", Component: ControlPanel, config: { - getPagerProps() { - return pagerInfo; - }, + pagerProps: pagerProps, }, searchMenuTypes: [], }); assert.containsOnce(controlPanel.el, ".o_pager"); - pagerInfo.total = 0; + pagerProps.total = 0; controlPanel.render(); await nextTick(); assert.containsNone(controlPanel.el, ".o_pager"); diff --git a/addons/web/static/tests/search/pager_hook_tests.js b/addons/web/static/tests/search/pager_hook_tests.js index 773852e0ba4..478fad93573 100644 --- a/addons/web/static/tests/search/pager_hook_tests.js +++ b/addons/web/static/tests/search/pager_hook_tests.js @@ -2,7 +2,7 @@ import { ControlPanel } from "@web/search/control_panel/control_panel"; import { usePager } from "@web/search/pager_hook"; -import { click } from "../helpers/utils"; +import { click, nextTick } from "../helpers/utils"; import { makeWithSearch, setupControlPanelServiceRegistry } from "./helpers"; const { Component, useState, xml } = owl; @@ -110,5 +110,22 @@ QUnit.module("Search", (hooks) => { offset: 10, limit: 10, }); + + comp.state.offset = 20; + await nextTick(); + + assert.containsOnce(comp, ".o_pager"); + assert.strictEqual( + comp.el.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(), + "21-30" + ); + assert.strictEqual( + comp.el.querySelector(`.o_pager_counter span.o_pager_limit`).textContent.trim(), + "50" + ); + assert.deepEqual(comp.state, { + offset: 20, + limit: 10, + }); }); }); From 221412630d6a12a8677ff9c9ffbafa3c85ebc548 Mon Sep 17 00:00:00 2001 From: Jorge Pinna Puissant Date: Fri, 25 Feb 2022 08:47:57 +0000 Subject: [PATCH 4/5] [FIX] web: extend the timeout when using QUnit.debug This commit will also ensure that the document is ready before adding the debug class. Part-of: odoo/odoo#84314 --- addons/web/static/tests/qunit.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/web/static/tests/qunit.js b/addons/web/static/tests/qunit.js index 8451d76ef9d..1677bd76157 100644 --- a/addons/web/static/tests/qunit.js +++ b/addons/web/static/tests/qunit.js @@ -309,8 +309,9 @@ }); QUnit.debug = (name, cb) => { - document.body.classList.add("debug"); + owl.whenReady(() => document.body.classList.add("debug")); QUnit.config.debug = true; + QUnit.config.testTimeout = 60 * 60 * 1000; QUnit.only(name, cb); }; From df944e3f9ce553b9fe82b0b4bca29ab174aabe03 Mon Sep 17 00:00:00 2001 From: Jorge Pinna Puissant Date: Fri, 25 Feb 2022 09:08:32 +0000 Subject: [PATCH 5/5] [REF] mail: remove the hook useShouldUpdateBasedOnProps With the new fine-grained reactivity in owl 2, this hook is not necessary any more. Part-of: odoo/odoo#84314 --- .../use_should_update_based_on_props.js | 49 ------------------- .../static/src/utils/messaging_component.js | 2 - 2 files changed, 51 deletions(-) delete mode 100644 addons/mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js diff --git a/addons/mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js b/addons/mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js deleted file mode 100644 index 90add18a6fa..00000000000 --- a/addons/mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js +++ /dev/null @@ -1,49 +0,0 @@ -/** @odoo-module **/ - -const { onPatched, useComponent } = owl; - -/** - * Shallow compares props `a` and `b`. - * - * @param {Object} a - * @param {Object} b - * @returns {boolean} - */ -function isEqual(a, b) { - const keys = Object.keys(a); - if (Object.keys(b).length !== keys.length) { - return false; - } - for (const key of keys) { - if (a[key] !== b[key]) { - return false; - } - } - return true; -} - -/** - * This hook overrides the `shouldUpdate` method to ensure the component is only - * updated if its props actually changed. This is especially useful to use on - * components for which an extra render costs proportionally a lot more than - * comparing props. - */ -export function useShouldUpdateBasedOnProps() { - const component = useComponent(); - let forceRender = false; - component.shouldUpdate = nextProps => { - if (forceRender) { - return true; - } - const allNewProps = Object.assign({}, nextProps); - const defaultProps = component.constructor.defaultProps; - for (const key in defaultProps) { - if (allNewProps[key] === undefined) { - allNewProps[key] = defaultProps[key]; - } - } - forceRender = !isEqual(component.props, allNewProps); - return forceRender; - }; - onPatched(() => forceRender = false); -} diff --git a/addons/mail/static/src/utils/messaging_component.js b/addons/mail/static/src/utils/messaging_component.js index 6228913a1bf..e002f475229 100644 --- a/addons/mail/static/src/utils/messaging_component.js +++ b/addons/mail/static/src/utils/messaging_component.js @@ -1,7 +1,6 @@ /** @odoo-module */ import { useModels } from "@mail/component_hooks/use_models/use_models"; -import { useShouldUpdateBasedOnProps } from "@mail/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props"; const { useRef } = owl; @@ -29,7 +28,6 @@ export function registerMessagingComponent(ComponentClass) { setup() { this.root = useRef('root'); useModels(); - useShouldUpdateBasedOnProps(); super.setup(); } get className() {