Skip to content

Commit 2d327a1

Browse files
committed
[FIX] web: do not crash when saving if offline
Be in a form view, edit some fields, simulate a connection lost (e.g. with devtools network throttling). Leave the form, e.g. by clicking on the breadcrumb. Before this commit, there was a crash, because the web_save returned a ConnectionLostError and that error wasn't properly handled. With this commit, we remain in the form, and if the connection is back later on, the user is able to leave and save again. part of task~5106517 closes odoo#229492 Related: odoo/enterprise#96626 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
1 parent 600ec28 commit 2d327a1

File tree

2 files changed

+117
-0
lines changed

2 files changed

+117
-0
lines changed

addons/web/static/src/views/form/form_controller.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
} from "@odoo/owl";
4646
import { FetchRecordError } from "@web/model/relational_model/errors";
4747
import { effect } from "@web/core/utils/reactive";
48+
import { ConnectionLostError } from "@web/core/network/rpc";
4849

4950
const viewRegistry = registry.category("views");
5051

@@ -427,6 +428,9 @@ export class FormController extends Component {
427428
async onWillSaveRecord() {}
428429

429430
async onSaveError(error, { discard, retry }, leaving) {
431+
if (error instanceof ConnectionLostError) {
432+
return false;
433+
}
430434
const suggestedCompany = error.data?.context?.suggested_company;
431435
const activeCompanyIds = user.activeCompanies.map((c) => c.id);
432436
if (

addons/web/static/tests/views/form/form_view.test.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,119 @@ test(`[Offline] form switches to readonly in offline mode`, async () => {
298298
expect(`.o_field_x2many_list_row_add`).toHaveCount(1);
299299
});
300300

301+
test(`[Offline] save a form view offline (click save icon)`, async () => {
302+
let offline = false;
303+
onRpc(
304+
"/web/dataset/call_kw/partner/web_save",
305+
() => {
306+
expect.step("web_save");
307+
if (offline) {
308+
return new Response("", { status: 502 });
309+
}
310+
},
311+
{ pure: true }
312+
);
313+
314+
Partner._views = {
315+
form: `<form><field name="foo"/></form>`,
316+
list: `<list><field name="foo"/></list>`,
317+
search: `<search/>`,
318+
};
319+
defineActions([
320+
{
321+
id: 1,
322+
name: "Partner",
323+
res_model: "partner",
324+
views: [
325+
[false, "list"],
326+
[false, "form"],
327+
],
328+
},
329+
]);
330+
331+
await mountWithCleanup(WebClient);
332+
await getService("action").doAction(1);
333+
334+
await contains(".o_data_row .o_data_cell").click();
335+
expect(".o_form_renderer").toHaveClass("o_form_editable");
336+
expect(".o_field_widget[name=foo] input").toHaveValue("yop");
337+
await contains(".o_field_widget[name=foo] input").edit("new foo");
338+
339+
offline = true;
340+
await contains(".o_form_button_save").click();
341+
expect(".o_form_renderer").toHaveClass("o_form_readonly");
342+
expect(".o_field_widget[name=foo]").toHaveText("new foo");
343+
expect(getService("offline").status.offline).toBe(true);
344+
expect.verifySteps(["web_save"]);
345+
346+
offline = false;
347+
getService("offline").status.offline = false;
348+
await animationFrame();
349+
expect(".o_form_renderer").toHaveClass("o_form_editable");
350+
await contains(".o_form_button_save").click();
351+
expect.verifySteps(["web_save"]);
352+
353+
await contains(".o_breadcrumb .o_back_button").click();
354+
expect(".o_data_cell:first").toHaveText("new foo");
355+
});
356+
357+
test(`[Offline] save a form view offline (autosave when leaving)`, async () => {
358+
// this test is the same as above, but in this one we don't manually save
359+
// the record before leaving
360+
let offline = false;
361+
onRpc(
362+
"/web/dataset/call_kw/partner/web_save",
363+
() => {
364+
expect.step("web_save");
365+
if (offline) {
366+
return new Response("", { status: 502 });
367+
}
368+
},
369+
{ pure: true }
370+
);
371+
372+
Partner._views = {
373+
form: `<form><field name="foo"/></form>`,
374+
list: `<list><field name="foo"/></list>`,
375+
search: `<search/>`,
376+
};
377+
defineActions([
378+
{
379+
id: 1,
380+
name: "Partner",
381+
res_model: "partner",
382+
views: [
383+
[false, "list"],
384+
[false, "form"],
385+
],
386+
},
387+
]);
388+
389+
await mountWithCleanup(WebClient);
390+
await getService("action").doAction(1);
391+
392+
await contains(".o_data_row .o_data_cell").click();
393+
expect(".o_form_renderer").toHaveClass("o_form_editable");
394+
expect(".o_field_widget[name=foo] input").toHaveValue("yop");
395+
await contains(".o_field_widget[name=foo] input").edit("new foo");
396+
397+
offline = true;
398+
await contains(".o_breadcrumb .o_back_button").click();
399+
expect(".o_form_renderer").toHaveClass("o_form_readonly");
400+
expect(".o_field_widget[name=foo]").toHaveText("new foo");
401+
expect(getService("offline").status.offline).toBe(true);
402+
expect.verifySteps(["web_save"]);
403+
404+
offline = false;
405+
getService("offline").status.offline = false;
406+
await animationFrame();
407+
expect(".o_form_renderer").toHaveClass("o_form_editable");
408+
409+
await contains(".o_breadcrumb .o_back_button").click();
410+
expect(".o_data_cell:first").toHaveText("new foo");
411+
expect.verifySteps(["web_save"]);
412+
});
413+
301414
test(`form rendering with class and style attributes`, async () => {
302415
await mountView({
303416
resModel: "partner",

0 commit comments

Comments
 (0)