[FIX] web: concurrency issue when opening form view

From a list or kanban view, quickly click multiple times on a
record or on the Create button: a lot of bad stuff could happen
from there (e.g. crashes, form view that could not be open
anymore afterward).

The reason was that we destroyed the legacy form view whereas we
shouldn't, as it was still used. In more details, when we clicked
first on a record to open it, we created an owl Component to
instantiate and wrap the form view (legacy). When we clicked a
second time, we another owl Component was instantiated and the
first one was destroyed. As an unwanted side-effect, the legacy
form view was destroyed as well, but the new controller wanted to
reuse it. From that point, anything bad could happen as we were
trying to use a destroyed widget.

closes odoo-dev/odoo#941

Signed-off-by: Géry Debongnie (ged) <ged@openerp.com>
This commit is contained in:
Aaron Bohy
2021-06-18 21:31:32 +02:00
parent aab6f88439
commit 840f914a4e
3 changed files with 59 additions and 1 deletions
@@ -69,6 +69,10 @@ class ActionAdapter extends ComponentAdapter {
);
}
get actionId() {
throw new Error("Should be implement by specific adapters");
}
_trigger_up(ev) {
const payload = ev.data;
if (ev.name === "do_action") {
@@ -147,6 +151,12 @@ class ActionAdapter extends ComponentAdapter {
}
super.willUnmount();
}
__destroy() {
if (this.actionService.__legacy__isActionInStack(this.actionId)) {
this.widget = null;
}
super.__destroy(...arguments);
}
}
export class ClientActionAdapter extends ActionAdapter {
@@ -156,6 +166,10 @@ export class ClientActionAdapter extends ActionAdapter {
this.env = Component.env;
}
get actionId() {
return this.props.widgetArgs[0].jsId;
}
async willStart() {
if (this.props.widget) {
this.widget = this.props.widget;
@@ -240,6 +254,10 @@ export class ViewAdapter extends ActionAdapter {
this.env = Component.env;
}
get actionId() {
return this.props.viewParams.action.jsId;
}
async willStart() {
if (this.props.widget) {
this.widget = this.props.widget;
@@ -254,7 +272,9 @@ export class ViewAdapter extends ActionAdapter {
if (this.__owl__.status === 5 /* DESTROYED */) {
// the component might have been destroyed meanwhile, but if so, `this.widget` wasn't
// destroyed by OwlCompatibility layer as it wasn't set yet, so destroy it now
this.widget.destroy();
if (!this.actionService.__legacy__isActionInStack(this.actionId)) {
this.widget.destroy();
}
return Promise.resolve();
}
return this.widget._widgetRenderAndInsert(() => {});
@@ -1215,6 +1215,9 @@ function makeActionManager(env) {
get currentController() {
return _getCurrentController();
},
__legacy__isActionInStack(actionId) {
return controllerStack.find((c) => c.action.jsId === actionId);
},
};
}
@@ -582,4 +582,39 @@ QUnit.module("ActionManager", (hooks) => {
"/web/dataset/search_read",
]);
});
QUnit.test("click multiple times to open a record", async function (assert) {
assert.expect(5);
const def = testUtils.makeTestPromise();
const defs = [null, def];
const mockRPC = async (route, args) => {
if (args.method === "read") {
await Promise.resolve(defs.shift());
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
assert.containsOnce(webClient, ".o_list_view");
await testUtils.dom.click(webClient.el.querySelector(".o_list_view .o_data_row"));
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_form_view");
await testUtils.dom.click(webClient.el.querySelector(".o_back_button"));
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_list_view");
await testUtils.dom.click(webClient.el.querySelector(".o_list_view .o_data_row"));
await testUtils.dom.click(webClient.el.querySelector(".o_list_view .o_data_row"));
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_list_view");
def.resolve();
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(webClient, ".o_form_view");
});
});