[REF] *: adapt code to owl2 with fine-grained reactivity

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 <ged@odoo.com>
This commit is contained in:
Jorge Pinna Puissant
2022-02-25 12:35:43 +00:00
co-authored by Géry Debongnie
parent d90f481448
commit 09b4668c6b
21 changed files with 96 additions and 50 deletions
@@ -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();
},
});
@@ -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);
+5 -3
View File
@@ -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);
}
}
@@ -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">
+6 -2
View File
@@ -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());
});
}
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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,
};
@@ -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,
});
});
});