[FIX] web(_editor): protect RPCs from component destruction

The rpc service has a protection (when used via useService) that
prevents the returned promise from being resolved/rejected if the
component has been destroyed meanwhile.

However, as the FormView still uses the legacy BasicModel, all rpcs
done by its model do not go through our new rpc service. As a
consequence, before this commit, they did not benefit from the
protection. This caused a non deterministic issue in the main flow
tour.

This commit implements a similar protection as the rpc service, but
specialized for the BasicRelationalModel, which redirects all RPCs
done by the BasicModel.

This change highlighted two scenarios where we sometimes performed
RPCs (or could perform RPCs) on a destroyed component. This commit
fixes those as well.

X-original-commit: f613e916d5e8b465fc360e340094c09037450ff1
Part-of: odoo/odoo#105332
This commit is contained in:
Aaron Bohy
2022-11-08 15:56:08 +01:00
parent d8b53f715e
commit 9db9dbd38a
5 changed files with 26 additions and 7 deletions
@@ -141,10 +141,9 @@ var ServicesMixin = {
prom.abort = function () {};
}
var abort = prom.abort ? prom.abort : prom.reject;
if (!abort) {
throw new Error("a rpc promise should always have a reject function");
if (abort) {
prom.abort = abort.bind(prom);
}
prom.abort = abort.bind(prom);
return prom;
},
loadFieldView: function (modelName, context, view_id, view_type, options) {
@@ -1099,6 +1099,8 @@ export class RelationalModel extends Model {
throw "only record root type is supported";
}
this.__component = params.component;
this.root = null;
this.__bm__ = new BasicModel(this, {
@@ -1320,7 +1322,25 @@ export class RelationalModel extends Model {
if (payload.service === "ajax" && payload.method === "rpc") {
// ajax service uses an extra 'target' argument for rpc
args = args.concat(ev.target);
return payload.callback(owl.Component.env.session.rpc(...args));
if (owl.status(this.__component) === "destroyed") {
console.warn("Component is destroyed");
return payload.callback(Promise.resolve());
}
const prom = new Promise((resolve, reject) => {
owl.Component.env.session
.rpc(...args)
.then((value) => {
if (owl.status(this.__component) !== "destroyed") {
resolve(value);
}
})
.guardedCatch((reason) => {
if (owl.status(this.__component) !== "destroyed") {
reject(reason);
}
});
});
return payload.callback(prom);
} else if (payload.service === "notification") {
return this.notificationService.add(payload.message, {
className: payload.className,
@@ -130,6 +130,7 @@ export class FormController extends Component {
rootType: "record",
mode,
beforeLoadProm,
component: this,
},
{
ignoreUseSampleModel: true,
@@ -4,7 +4,7 @@ import { useService } from "@web/core/utils/hooks";
import { evaluateExpr } from "@web/core/py_js/py";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { useEnv, useSubEnv } from "@odoo/owl";
import { status, useEnv, useSubEnv } from "@odoo/owl";
function disableButtons(el) {
// WOWL: can we do this non-imperatively?
@@ -86,7 +86,7 @@ export function useViewButtons(model, ref, options = {}) {
context: params.context || {}, //LPE FIXME new Context(payload.env.context).eval();
buttonContext,
onClose: async () => {
if (!closeDialog) {
if (!closeDialog && status(comp) !== "destroyed") {
const reload = options.reload || (() => model.root.load());
await reload();
comp.render(true); // FIXME WOWL reactivity
@@ -160,7 +160,6 @@ export class HtmlField extends Component {
if (this.resizerHandleObserver) {
this.resizerHandleObserver.disconnect();
}
this.updateValue();
});
}