[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:
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user