[MERGE][REF] *: update owl to version 2 with fine-grained reactivity
This PR updates owl to version 2 with fine-grained reactivity, and adapts the code in Odoo accordingly. closes odoo/odoo#84314 Related: odoo/enterprise#24218 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com> Co-authored-by: Géry Debongnie [ged@odoo.com](mailto:ged@odoo.com)
This commit is contained in:
co-authored by
Géry Debongnie [ged@odoo.com](mailto:ged@odoo.com)
commit
98e8e8a5f5
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
-49
@@ -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);
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="project.ProjectSharingWebClient" owl="1">
|
||||
<NotUpdatable>
|
||||
<ActionContainer />
|
||||
</NotUpdatable>
|
||||
<ActionContainer />
|
||||
<MainComponentsContainer/>
|
||||
</t>
|
||||
|
||||
|
||||
+284
-248
@@ -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';
|
||||
|
||||
|
||||
|
||||
@@ -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`
|
||||
<div class="o_dialog_container" t-att-class="{'modal-open': Object.keys(props.dialogs).length > 0}">
|
||||
<div>
|
||||
<t t-foreach="Object.values(props.dialogs)" t-as="dialog" t-key="dialog.id">
|
||||
<NotUpdatable>
|
||||
<ErrorHandler onError="(error) => this.handleError(error, dialog.id)">
|
||||
<t t-component="dialog.class" t-props="dialog.props" isActive="dialog_last"/>
|
||||
</ErrorHandler>
|
||||
</NotUpdatable>
|
||||
<ErrorHandler onError="(error) => this.handleError(error, dialog.id)">
|
||||
<t t-component="dialog.class" t-props="dialog.props" isActive="dialog_last"/>
|
||||
</ErrorHandler>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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`
|
||||
<div>
|
||||
<t t-foreach="Components" t-as="C" t-key="C[0]">
|
||||
<NotUpdatable>
|
||||
<ErrorHandler onError="error => this.handleComponentError(error, C)">
|
||||
<t t-component="C[1].Component" t-props="C[1].props"/>
|
||||
</ErrorHandler>
|
||||
</NotUpdatable>
|
||||
<ErrorHandler onError="error => this.handleComponentError(error, C)">
|
||||
<t t-component="C[1].Component" t-props="C[1].props"/>
|
||||
</ErrorHandler>
|
||||
</t>
|
||||
</div>
|
||||
`;
|
||||
MainComponentsContainer.components = { NotUpdatable, ErrorHandler };
|
||||
MainComponentsContainer.components = { ErrorHandler };
|
||||
|
||||
@@ -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`<t t-slot="default" />`;
|
||||
|
||||
export class ErrorHandler extends Component {
|
||||
setup() {
|
||||
onError((error) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,10 +29,7 @@
|
||||
</t>
|
||||
|
||||
<div class="o_cp_pager" role="search">
|
||||
<t t-set="pagerProps" t-value="env.config.getPagerProps()" />
|
||||
<t t-if="pagerProps">
|
||||
<Pager t-if="pagerProps.total > 0" t-props="pagerProps" />
|
||||
</t>
|
||||
<Pager t-if="pagerProps and pagerProps.total > 0" t-props="pagerProps"/>
|
||||
</div>
|
||||
|
||||
<t t-if="env.config.viewSwitcherEntries.length">
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -30,11 +30,9 @@
|
||||
<!-- Systray -->
|
||||
<div class="o_menu_systray d-flex flex-shrink-0 ml-auto" role="menu">
|
||||
<t t-foreach="systrayItems" t-as="item" t-key="item.key">
|
||||
<NotUpdatable>
|
||||
<ErrorHandler onError="error => this.handleItemError(error, item)">
|
||||
<t t-component="item.Component" t-props="item.props"/>
|
||||
</ErrorHandler>
|
||||
</NotUpdatable>
|
||||
<ErrorHandler onError="error => this.handleItemError(error, item)">
|
||||
<t t-component="item.Component" t-props="item.props"/>
|
||||
</ErrorHandler>
|
||||
</t>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.WebClient" owl="1">
|
||||
<NotUpdatable t-if="!state.fullscreen">
|
||||
<t t-if="!state.fullscreen">
|
||||
<NavBar/>
|
||||
</NotUpdatable>
|
||||
<NotUpdatable>
|
||||
<ActionContainer/>
|
||||
</NotUpdatable>
|
||||
</t>
|
||||
<ActionContainer/>
|
||||
<MainComponentsContainer/>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -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`<div>hey</div>`;
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Child/>
|
||||
<NotUpdatable><Child/></NotUpdatable>
|
||||
</div>`;
|
||||
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`<div><t t-esc="this.will.throw"/></div>`;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user