[FIX] web: restore open notebook page

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) <lpe@odoo.com>
Signed-off-by: Georis François (fge) <fge@odoo.com>
This commit is contained in:
FrancoisGe
2022-10-07 14:36:52 +02:00
parent 106dc30beb
commit 5cf5e8fdd3
7 changed files with 237 additions and 6 deletions
@@ -52,6 +52,7 @@ export class FormCompiler extends ViewCompiler {
this.encounteredFields = {};
/** @type {Record<string, Element[]>} */
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^=\\#]")) {
@@ -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() {
@@ -37,7 +37,7 @@
<button type="button" class="btn btn-outline-primary o_form_button_create" data-hotkey="c" t-on-click.stop="create" t-att-disabled="state.isDisabled">Create</button>
</t>
</t>
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="archInfo" translateAlert="translateAlert" enableViewButtons.bind="enableButtons" disableViewButtons.bind="disableButtons"/>
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="archInfo" translateAlert="translateAlert" enableViewButtons.bind="enableButtons" disableViewButtons.bind="disableButtons" onNotebookPageChange.bind="onNotebookPageChange" activeNotebookPages="props.state and props.state.activeNotebookPages"/>
</Layout>
</div>
</div>
@@ -83,3 +83,7 @@ FormRenderer.components = {
InnerGroup,
StatusBarButtons,
};
FormRenderer.defaultProps = {
activeNotebookPages: {},
onNotebookPageChange: () => {},
};
@@ -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": `
<form>
<notebook>
<page string="Yop">
<field name="display_name">
</field>
</page>
</notebook>
</form>`,
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="turtles">
<tree>
<field name="turtle_foo"/>
</tree>
</field>
</form>`,
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": '<form><field name="foo"/></form>',
@@ -115,7 +115,7 @@ QUnit.module("Form Compiler", (hooks) => {
</form>`;
const expected = /*xml*/ `
<Notebook>
<Notebook defaultPage="props.record.isNew ? undefined : props.activeNotebookPages[0]" onPageUpdate="(page) =&gt; this.props.onNotebookPageChange(0, page)">
<t t-set-slot="page_1" title="\`Page1\`" name="\`p1\`" isVisible="true">
<Field id="'charfield'" name="'charfield'" record="props.record" fieldInfo="props.archInfo.fieldNodes['charfield']"/>
</t>
@@ -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": `<tree><field name="foo"/></tree>`,
"partner,false,search": `<search></search>`,
"partner,false,form": `
<form>
<notebook>
<page string="First Page" name="first">
<field name="foo"/>
</page>
<page string="Second page" name="second">
<field name="bar"/>
</page>
</notebook>
<notebook>
<page string="Page1" name="p1">
<field name="foo"/>
</page>
<page string="Page2" name="p2" autofocus="autofocus">
<field name="bar"/>
</page>
<page string="Page3" name="p3">
<field name="bar"/>
</page>
</notebook>
</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": `<tree><field name="foo"/></tree>`,
"partner,false,search": `<search></search>`,
"partner,false,form": `
<form>
<notebook>
<page string="First Page" name="first">
<field name="foo"/>
</page>
<page string="Second page" name="second">
<field name="bar"/>
</page>
</notebook>
<notebook>
<page string="Page1" name="p1">
<field name="foo"/>
</page>
<page string="Page2" name="p2" autofocus="autofocus">
<field name="bar"/>
</page>
<page string="Page3" name="p3">
<field name="bar"/>
</page>
</notebook>
</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",