From 5cf5e8fdd3aa643a67eeadf11d4b986052ca8cc1 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Thu, 6 Oct 2022 09:33:42 +0000 Subject: [PATCH] [FIX] web: restore open notebook page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The purpose of this commit is to allow the restoration of open pages in notebooks of a form view when switching view. How to reproduce? - Go to a form view with a notebook. - Select a page in the notebook - Go to another view - Use the breadcrumb to go back to the previous form view Before this commit: The page opened in the notebook is the default one (the first one or the autofocus one) After this commit: The page opened in the notebook is the one previously selected closes odoo/odoo#102468 X-original-commit: 160c0347526612b6e6f93b8079eeb868d37d48f4 Signed-off-by: Lucas Perais (lpe) Signed-off-by: Georis François (fge) --- .../static/src/views/form/form_compiler.js | 16 +- .../static/src/views/form/form_controller.js | 13 +- .../static/src/views/form/form_controller.xml | 2 +- .../static/src/views/form/form_renderer.js | 4 + .../views/fields/one2many_field_tests.js | 40 +++++ .../tests/views/form/form_compiler_tests.js | 2 +- .../tests/views/form/form_view_tests.js | 166 ++++++++++++++++++ 7 files changed, 237 insertions(+), 6 deletions(-) diff --git a/addons/web/static/src/views/form/form_compiler.js b/addons/web/static/src/views/form/form_compiler.js index eebcdba4b95..3873fcea81c 100644 --- a/addons/web/static/src/views/form/form_compiler.js +++ b/addons/web/static/src/views/form/form_compiler.js @@ -52,6 +52,7 @@ export class FormCompiler extends ViewCompiler { this.encounteredFields = {}; /** @type {Record} */ this.labels = {}; + this.noteBookId = 0; this.compilers.push( ...compilersRegistry.getAll(), { selector: "div[name='button_box']", fn: this.compileButtonBox }, @@ -481,6 +482,7 @@ export class FormCompiler extends ViewCompiler { * @returns {Element} */ compileNotebook(el, params) { + const noteBookId = this.noteBookId++; const noteBook = createElement("Notebook"); const pageAnchors = [...document.querySelectorAll("[href^=\\#]")] .map((a) => CSS.escape(a.getAttribute("href").substring(1))) @@ -492,6 +494,15 @@ export class FormCompiler extends ViewCompiler { el.removeAttribute("class"); } + noteBook.setAttribute( + "defaultPage", + `props.record.isNew ? undefined : props.activeNotebookPages[${noteBookId}]` + ); + noteBook.setAttribute( + "onPageUpdate", + `(page) => this.props.onNotebookPageChange(${noteBookId}, page)` + ); + for (const child of el.children) { if (getTag(child, true) !== "page") { continue; @@ -518,7 +529,10 @@ export class FormCompiler extends ViewCompiler { } if (child.getAttribute("autofocus") === "autofocus") { - noteBook.setAttribute("defaultPage", `"${pageId}"`); + noteBook.setAttribute( + "defaultPage", + `props.record.isNew ? "${pageId}" : (props.activeNotebookPages[${noteBookId}] || "${pageId}")` + ); } for (const anchor of child.querySelectorAll("[href^=\\#]")) { diff --git a/addons/web/static/src/views/form/form_controller.js b/addons/web/static/src/views/form/form_controller.js index c823cee8bcf..dde612e9c0c 100644 --- a/addons/web/static/src/views/form/form_controller.js +++ b/addons/web/static/src/views/form/form_controller.js @@ -172,6 +172,15 @@ export class FormController extends Component { beforeExecuteAction: this.beforeExecuteActionButton.bind(this), afterExecuteAction: this.afterExecuteActionButton.bind(this), }); + + const state = this.props.state || {}; + const { fieldsToTranslate } = state; + this.fieldsToTranslate = useState(fieldsToTranslate || {}); + const activeNotebookPages = { ...state.activeNotebookPages }; + this.onNotebookPageChange = (notebookId, page) => { + activeNotebookPages[notebookId] = page; + }; + useSetupView({ rootRef, beforeLeave: () => { @@ -187,6 +196,7 @@ export class FormController extends Component { getLocalState: () => { // TODO: export the whole model? return { + activeNotebookPages: !this.model.root.isNew && activeNotebookPages, resId: this.model.root.resId, fieldsToTranslate: toRaw(this.fieldsToTranslate), }; @@ -242,9 +252,6 @@ export class FormController extends Component { () => [this.model.root.isInEdition] ); } - - const { fieldsToTranslate } = this.props.state || {}; - this.fieldsToTranslate = useState(fieldsToTranslate || {}); } displayName() { diff --git a/addons/web/static/src/views/form/form_controller.xml b/addons/web/static/src/views/form/form_controller.xml index 1c5e72bfcc9..f8594e7f0ba 100644 --- a/addons/web/static/src/views/form/form_controller.xml +++ b/addons/web/static/src/views/form/form_controller.xml @@ -37,7 +37,7 @@ - + diff --git a/addons/web/static/src/views/form/form_renderer.js b/addons/web/static/src/views/form/form_renderer.js index eccf98f060f..ee1d3ad58b3 100644 --- a/addons/web/static/src/views/form/form_renderer.js +++ b/addons/web/static/src/views/form/form_renderer.js @@ -83,3 +83,7 @@ FormRenderer.components = { InnerGroup, StatusBarButtons, }; +FormRenderer.defaultProps = { + activeNotebookPages: {}, + onNotebookPageChange: () => {}, +}; diff --git a/addons/web/static/tests/views/fields/one2many_field_tests.js b/addons/web/static/tests/views/fields/one2many_field_tests.js index 8d7a51e9ff4..54e927644f2 100644 --- a/addons/web/static/tests/views/fields/one2many_field_tests.js +++ b/addons/web/static/tests/views/fields/one2many_field_tests.js @@ -6426,6 +6426,46 @@ QUnit.module("Fields", (hooks) => { } ); + QUnit.test( + "open a record in a one2many list (mode 'readonly') with a notebook", + async function (assert) { + serverData.views = { + "turtle,false,form": ` +
+ + + + + + +
`, + }; + + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
+ + + + + +
`, + resId: 1, + }); + + await click(target, ".o_data_cell"); + assert.containsOnce(target, ".modal .o_form_view"); + assert.containsOnce(target, ".modal .o_form_view .o_notebook_headers"); + assert.strictEqual( + target.querySelector(".modal .o_form_view .o_notebook_headers").textContent, + "Yop" + ); + } + ); + QUnit.test("one2many field with virtual ids", async function (assert) { serverData.views = { "partner,false,form": '
', diff --git a/addons/web/static/tests/views/form/form_compiler_tests.js b/addons/web/static/tests/views/form/form_compiler_tests.js index ee5fe8a603d..d05edfd091a 100644 --- a/addons/web/static/tests/views/form/form_compiler_tests.js +++ b/addons/web/static/tests/views/form/form_compiler_tests.js @@ -115,7 +115,7 @@ QUnit.module("Form Compiler", (hooks) => { `; const expected = /*xml*/ ` - + diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index fb6927b5ef4..d18cdfdc6d5 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -4916,6 +4916,172 @@ QUnit.module("Views", (hooks) => { assert.hasClass(target.querySelectorAll(".o_notebook .nav-link")[1], "active"); }); + QUnit.test( + "restore the open notebook page when switching to another view", + async function (assert) { + serverData.actions = { + 1: { + id: 1, + name: "test", + res_model: "partner", + type: "ir.actions.act_window", + views: [[false, "list"]], + }, + 2: { + id: 2, + name: "test2", + res_model: "partner", + res_id: 1, + type: "ir.actions.act_window", + views: [[false, "form"]], + }, + }; + serverData.views = { + "partner,false,list": ``, + "partner,false,search": ``, + "partner,false,form": ` +
+ + + + + + + + + + + + + + + + + + + +
`, + }; + + const webClient = await createWebClient({ serverData }); + await doAction(webClient, 2); + + let notebooks = target.querySelectorAll(".o_notebook"); + assert.hasClass(notebooks[0].querySelector(".nav-link"), "active"); + assert.doesNotHaveClass(notebooks[0].querySelectorAll(".nav-link")[1], "active"); + + assert.doesNotHaveClass(notebooks[1].querySelector(".nav-link"), "active"); + assert.hasClass(notebooks[1].querySelectorAll(".nav-link")[1], "active"); + assert.doesNotHaveClass(notebooks[1].querySelectorAll(".nav-link")[2], "active"); + + // click on second page tab of the first notebook + await click(notebooks[0].querySelectorAll(".nav-link")[1]); + // click on third page tab of the second notebook + await click(notebooks[1].querySelectorAll(".nav-link")[2]); + notebooks = target.querySelectorAll(".o_notebook"); + assert.doesNotHaveClass(notebooks[0].querySelector(".nav-link"), "active"); + assert.hasClass(notebooks[0].querySelectorAll(".nav-link")[1], "active"); + + assert.doesNotHaveClass(notebooks[1].querySelector(".nav-link"), "active"); + assert.doesNotHaveClass(notebooks[1].querySelectorAll(".nav-link")[1], "active"); + assert.hasClass(notebooks[1].querySelectorAll(".nav-link")[2], "active"); + + // switch to a list view + await doAction(webClient, 1); + + // back to the form view + await click(target, ".o_back_button"); + notebooks = target.querySelectorAll(".o_notebook"); + assert.doesNotHaveClass(notebooks[0].querySelector(".nav-link"), "active"); + assert.hasClass(notebooks[0].querySelectorAll(".nav-link")[1], "active"); + + assert.doesNotHaveClass(notebooks[1].querySelector(".nav-link"), "active"); + assert.doesNotHaveClass(notebooks[1].querySelectorAll(".nav-link")[1], "active"); + assert.hasClass(notebooks[1].querySelectorAll(".nav-link")[2], "active"); + } + ); + + QUnit.test( + "don't restore the open notebook page when we create a new record", + async function (assert) { + serverData.actions = { + 1: { + id: 1, + name: "test", + res_model: "partner", + type: "ir.actions.act_window", + views: [ + [false, "list"], + [false, "form"], + ], + }, + }; + serverData.views = { + "partner,false,list": ``, + "partner,false,search": ``, + "partner,false,form": ` +
+ + + + + + + + + + + + + + + + + + + +
`, + }; + + const webClient = await createWebClient({ serverData }); + await doAction(webClient, 1); + await click(target.querySelector(".o_data_cell")); + + let notebooks = target.querySelectorAll(".o_notebook"); + assert.hasClass(notebooks[0].querySelector(".nav-link"), "active"); + assert.doesNotHaveClass(notebooks[0].querySelectorAll(".nav-link")[1], "active"); + + assert.doesNotHaveClass(notebooks[1].querySelector(".nav-link"), "active"); + assert.hasClass(notebooks[1].querySelectorAll(".nav-link")[1], "active"); + assert.doesNotHaveClass(notebooks[1].querySelectorAll(".nav-link")[2], "active"); + + // click on second page tab of the first notebook + await click(notebooks[0].querySelectorAll(".nav-link")[1]); + // click on third page tab of the second notebook + await click(notebooks[1].querySelectorAll(".nav-link")[2]); + notebooks = target.querySelectorAll(".o_notebook"); + assert.doesNotHaveClass(notebooks[0].querySelector(".nav-link"), "active"); + assert.hasClass(notebooks[0].querySelectorAll(".nav-link")[1], "active"); + + assert.doesNotHaveClass(notebooks[1].querySelector(".nav-link"), "active"); + assert.doesNotHaveClass(notebooks[1].querySelectorAll(".nav-link")[1], "active"); + assert.hasClass(notebooks[1].querySelectorAll(".nav-link")[2], "active"); + + // back to the list view + await click(target, ".o_back_button"); + + // Create a new record + await click(target, ".o_list_button_add"); + notebooks = target.querySelectorAll(".o_notebook"); + assert.hasClass(notebooks[0].querySelector(".nav-link"), "active"); + assert.doesNotHaveClass(notebooks[0].querySelectorAll(".nav-link")[1], "active"); + + assert.doesNotHaveClass(notebooks[1].querySelector(".nav-link"), "active"); + assert.hasClass(notebooks[1].querySelectorAll(".nav-link")[1], "active"); + assert.doesNotHaveClass(notebooks[1].querySelectorAll(".nav-link")[2], "active"); + } + ); + QUnit.test("pager is hidden in create mode", async function (assert) { await makeView({ type: "form",