From 9c8bff35bef9cf29ef1133e7ee850c0f21112f37 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Wed, 5 Oct 2022 11:20:37 +0000 Subject: [PATCH] [FIX] web: throw an error when saving a record in a list view Before this commit, in a list view, it is possible to have two records in edit mode at the same time if an error is thrown when saving the editedRecord. The editedRecord is the only list record that can be in edit mode. Why: If a server error is returned when saving the editedRecord, then it stays in edit mode but is no longer considered the editedRecord of the list. It is therefore possible to switch another record to edit mode. Solution: The record that fails its save due to the error should stay the editedRecord of the list. How to reproduce: - Go to a list view - Edit a record - Click on another record (the "write" operation returns an error) - Close the error dialog - Click again on the other record Before this commit: Both records are in edit mode After this commit: Only the edited commit is in edit mode. closes odoo/odoo#102560 X-original-commit: 77092167cf8f8fca11a39005d720fa82abeec0ae Signed-off-by: Aaron Bohy (aab) --- .../web/static/src/views/relational_model.js | 8 ++- .../web/static/tests/views/list_view_tests.js | 55 ++++++++++++++++--- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index af31a45ce4c..bdcde4357af 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -1424,7 +1424,13 @@ class DynamicList extends DataPoint { if (editedRecord !== record && editedRecord.canBeAbandoned) { this.abandonRecord(editedRecord.id); } else { - const isSaved = await editedRecord.save(); + let isSaved; + try { + isSaved = await editedRecord.save(); + } catch (e) { + this.editedRecord = editedRecord; + throw e; + } if (!isSaved) { this.editedRecord = editedRecord; return false; diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index e41f9c8f306..cc429388d61 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -2,12 +2,17 @@ import { browser } from "@web/core/browser/browser"; import { Domain } from "@web/core/domain"; +import { errorService } from "@web/core/errors/error_service"; import { localization } from "@web/core/l10n/localization"; import { registry } from "@web/core/registry"; -import { uiService } from "@web/core/ui/ui_service"; -import { session } from "@web/session"; -import { ListController } from "@web/views/list/list_controller"; import { tooltipService } from "@web/core/tooltip/tooltip_service"; +import { uiService } from "@web/core/ui/ui_service"; +import { getNextTabableElement } from "@web/core/utils/ui"; +import { session } from "@web/session"; +import { FloatField } from "@web/views/fields/float/float_field"; +import { TextField } from "@web/views/fields/text/text_field"; +import { ListController } from "@web/views/list/list_controller"; +import { DynamicRecordList } from "@web/views/relational_model"; import { actionService } from "@web/webclient/actions/action_service"; import { makeFakeLocalizationService, makeFakeUserService } from "../helpers/mock_services"; import { @@ -55,10 +60,6 @@ import { } from "../search/helpers"; import { createWebClient, doAction, loadState } from "../webclient/helpers"; import { makeView, setupViewRegistries } from "./helpers"; -import { getNextTabableElement } from "@web/core/utils/ui"; -import { FloatField } from "@web/views/fields/float/float_field"; -import { TextField } from "@web/views/fields/text/text_field"; -import { DynamicRecordList } from "@web/views/relational_model"; const { Component, onWillStart, xml } = owl; @@ -15537,4 +15538,44 @@ QUnit.module("Views", (hooks) => { "the widget has access to the record's data" ); }); + + QUnit.test( + "edit a record then select another record with a throw error when saving", + async function (assert) { + serviceRegistry.add("error", errorService); + + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: ` + + + `, + mockRPC(route, args) { + if (args.method === "write") { + throw new Error("Can't write"); + } + }, + }); + + await click(target.querySelectorAll(".o_data_cell")[1]); + await editInput(target, "[name='foo'] input", "plop"); + assert.containsOnce(target, "[name='foo'] input"); + + await click(target.querySelectorAll(".o_data_cell")[0]); + assert.containsOnce(target, ".o_dialog_error"); + + await click(target, ".o_dialog_error .btn-primary.o-default-button"); + assert.containsOnce(target, ".o_selected_row"); + assert.hasClass(target.querySelectorAll(".o_data_row")[1], "o_selected_row"); + + await click(target.querySelectorAll(".o_data_cell")[0]); + assert.containsOnce(target, ".o_dialog_error"); + + await click(target, ".o_dialog_error .btn-primary.o-default-button"); + assert.containsOnce(target, ".o_selected_row"); + assert.hasClass(target.querySelectorAll(".o_data_row")[1], "o_selected_row"); + } + ); });