[IMP] web: add Notebook component features

This commit improves the Notebook component to support the ability to
disable some tabs. Once the page is disabled, the tab is greyed out
and set as inactive. Also, an icon can be placed in the tab, before the
label.

This change is used since the Notebook component is used as the sidebar
element during the web_studio refactor. The sidebar needed to disable a
tab depending certain conditions. Now, the component has this behavior
by default once a page has the isDisabled attribute.

To display an icon before the label, you can use the icons props, as an
object, which keys are page names, and values are font awesome classnames.

A test has been added for each new feature introduced.

Part-of: odoo/odoo#115799
This commit is contained in:
luvi
2023-04-03 13:55:15 +02:00
committed by Lucas Perais
parent 6085480ea1
commit d80f45792d
4 changed files with 117 additions and 4 deletions
@@ -2,7 +2,14 @@
import { scrollTo } from "@web/core/utils/scrolling";
import { Component, onWillDestroy, onWillUpdateProps, useEffect, useRef, useState } from "@odoo/owl";
import {
Component,
onWillDestroy,
onWillUpdateProps,
useEffect,
useRef,
useState,
} from "@odoo/owl";
/**
* A notebook component that will render only the current page and allow
@@ -109,7 +116,9 @@ export class Notebook extends Component {
}
activatePage(pageIndex) {
this.state.currentPage = pageIndex;
if (!this.disabledPages.includes(pageIndex)) {
this.state.currentPage = pageIndex;
}
}
computePages(props) {
@@ -121,6 +130,7 @@ export class Notebook extends Component {
page.isVisible = true;
}
}
this.disabledPages = [];
const pages = [];
const pagesWithIndex = [];
for (const [k, v] of Object.entries({ ...props.slots, ...props.pages })) {
@@ -130,6 +140,9 @@ export class Notebook extends Component {
} else {
pages.push([id, v]);
}
if (v.isDisabled) {
this.disabledPages.push(k);
}
}
for (const page of pagesWithIndex) {
pages.splice(page[1].index, 0, page);
@@ -176,5 +189,6 @@ Notebook.props = {
anchors: { type: Object, optional: true },
defaultPage: { type: String, optional: true },
orientation: { type: String, optional: true },
icons: { type: Object, optional: true },
onPageUpdate: { type: Function, optional: true },
};
@@ -26,6 +26,13 @@
.nav-item {
white-space: nowrap;
margin: 0 -1px 0 0;
&.disabled {
.nav-link {
cursor: not-allowed;
opacity: .3;
}
}
}
.nav-link {
@@ -5,8 +5,11 @@
<div t-attf-class="o_notebook d-flex w-100 {{ props.orientation === 'horizontal' ? 'horizontal flex-column' : 'vertical flex-row' }} {{ props.className }}" t-if="state.currentPage">
<div class="o_notebook_headers" t-att-class="{ 'm-0': props.orientation === 'vertical' }">
<ul t-attf-class="nav nav-tabs {{ props.orientation === 'horizontal' ? 'flex-row flex-nowrap' : 'flex-column p-0' }}">
<li t-foreach="navItems" t-as="navItem" t-key="navItem[0]" class="nav-item flex-nowrap cursor-pointer" t-if="navItem[1].isVisible">
<a class="nav-link" t-attf-class="{{ navItem[0] === state.currentPage ? 'active' : '' }} {{ props.orientation === 'vertical' ? 'p-3 rounded-0' : '' }} {{ navItem[1].className }}" t-att-name="navItem[1].name" t-on-click.prevent="() => this.activatePage(navItem[0])" href="#" role="tab" tabindex="0" t-esc="navItem[1].title"/>
<li t-foreach="navItems" t-as="navItem" t-key="navItem[0]" class="nav-item flex-nowrap cursor-pointer" t-if="navItem[1].isVisible" t-attf-class="{{ navItem[1].isDisabled ? 'disabled' : '' }}">
<a class="nav-link" t-attf-class="{{ navItem[0] === state.currentPage ? 'active' : '' }} {{ props.orientation === 'vertical' ? 'p-3 rounded-0' : '' }} {{ navItem[1].className || '' }}" t-att-name="navItem[1].name" t-on-click.prevent="() => this.activatePage(navItem[0])" href="#" role="tab" tabindex="0">
<i t-if="props.icons and props.icons[navItem[0]]" t-attf-class="fa {{ props.icons[navItem[0]] }} me-2" />
<t t-esc="navItem[1].title" />
</a>
</li>
</ul>
</div>
@@ -334,4 +334,93 @@ QUnit.module("Components", (hooks) => {
assert.containsOnce(target, ".page2");
assert.strictEqual(target.querySelector(".nav-link.active").textContent, "page2");
});
QUnit.test("disabled pages are greyed out and can't be toggled", async (assert) => {
class Parent extends Component {}
Parent.components = { Notebook };
Parent.template = xml`
<Notebook defaultPage="'1'">
<t t-set-slot="1" title="'page1'" isVisible="true">
<div class="page1" />
</t>
<t t-set-slot="2" title="'page2'" isVisible="true" isDisabled="true">
<div class="page2" />
</t>
<t t-set-slot="3" title="'page3'" isVisible="true">
<div class="page3" />
</t>
</Notebook>`;
const env = await makeTestEnv();
await mount(Parent, target, { env });
assert.containsOnce(target, ".page1", "the default page is displayed");
assert.hasClass(
target.querySelector(".nav-item:nth-child(2)"),
"disabled",
"tab of the disabled page is greyed out"
);
await click(target.querySelector(".nav-item:nth-child(2) .nav-link"));
assert.containsOnce(target, ".page1", "the same page is still displayed");
await click(target.querySelector(".nav-item:nth-child(3) .nav-link"));
assert.containsOnce(target, ".page3", "the third page is now displayed");
});
QUnit.test("icons can be given for each page tab", async (assert) => {
class Parent extends Component {
get icons() {
return {
1: "fa-trash",
3: "fa-pencil",
};
}
}
Parent.components = { Notebook };
Parent.template = xml`
<Notebook defaultPage="'1'" icons="icons">
<t t-set-slot="1" title="'page1'" isVisible="true">
<div class="page1" />
</t>
<t t-set-slot="2" title="'page2'" isVisible="true">
<div class="page2" />
</t>
<t t-set-slot="3" title="'page3'" isVisible="true">
<div class="page3" />
</t>
</Notebook>`;
const env = await makeTestEnv();
await mount(Parent, target, { env });
assert.hasClass(
target.querySelector(".nav-item:nth-child(1) i"),
"fa-trash",
"tab of the first page has the given icon"
);
assert.strictEqual(
target.querySelector(".nav-item:nth-child(1)").textContent,
"page1",
"tab of the second page has the right text"
);
assert.containsNone(
target.querySelector(".nav-item:nth-child(2)"),
"i",
"tab of the second page doesn't have an icon"
);
assert.strictEqual(
target.querySelector(".nav-item:nth-child(2)").textContent,
"page2",
"tab of the second page has the right text"
);
assert.hasClass(
target.querySelector(".nav-item:nth-child(3) i"),
"fa-pencil",
"tab of the third page has the given icon"
);
assert.strictEqual(
target.querySelector(".nav-item:nth-child(3)").textContent,
"page3",
"tab of the second page has the right text"
);
});
});