[REF] web, *: improve view scale selector style and behavior

*: hr_holidays, hr_work_entry_contract

The commit introduces a new standard view component made to implement
in a consistant way the scale/select the appropriate period to display
in a view. This component is currently used by the Calendar view.

Tests have been added for this new component, as its behavior specs must
remain comprehensive. When there is no options or only one, the button is
removed from the view to keep space available, as its purpose is reduced.

This component gives the opportunity to keep the same style and behaviors
for views implementing it (eg: Calendar, Gantt) and should be used in
other views as well as soon as they are converted to Owl.

Other modules have been adapted to use the component name in xpath instead
of the span with the o_calendar_scale_buttons element. Tests have been
adapted as well.

A scss file has been modified to prevent the arrow to be misplaced on touch
devices due to a higher padding in the dropdown item element.

part of task #2867569

closes odoo/odoo#109090

Related: odoo/enterprise#35435
Signed-off-by: Bruno Boi (boi) <boi@odoo.com>
This commit is contained in:
luvi
2023-01-11 14:30:46 +01:00
parent bf0001ea3d
commit b31daf2f7b
10 changed files with 189 additions and 37 deletions
@@ -6,7 +6,7 @@
</t>
<t t-name="hr_holidays.CalendarController.controlButtons" t-inherit="web.CalendarController.controlButtons" t-inherit-mode="primary" owl="1">
<xpath expr="//span[hasclass('o_calendar_scale_buttons')]" position="after">
<xpath expr="//ViewScaleSelector" position="after">
<span class="o_timeoff_buttons">
<button class="btn btn-primary btn-time-off mx-1" t-on-click="newTimeOffRequest" type="button">
New Time Off
@@ -2,7 +2,7 @@
<templates>
<t t-name="hr_work_entry_contract.calendar.controlButtons" t-inherit="web.CalendarController.controlButtons" t-inherit-mode="primary" owl="1">
<xpath expr="//span[hasclass('o_calendar_scale_buttons')]" position="after">
<xpath expr="//ViewScaleSelector" position="after">
<span class="btn-regenerate-work-entries">
<button class="btn btn-secondary" t-on-click="onRegenerateWorkEntries">
Regenerate Work Entries
@@ -1,12 +1,11 @@
/** @odoo-module **/
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { _lt, _t } from "@web/core/l10n/translation";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
import { useModel } from "@web/views/model";
import { ViewScaleSelector } from "@web/views/view_components/view_scale_selector";
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
import { useSetupView } from "@web/views/view_hook";
import { CalendarDatePicker } from "./date_picker/calendar_date_picker";
@@ -89,8 +88,10 @@ export class CalendarController extends Component {
toggleSideBar: () => (this.state.showSideBar = !this.state.showSideBar),
};
}
get scaleLabels() {
return SCALE_LABELS;
get scales() {
return Object.fromEntries(
this.model.scales.map((s) => [s, { description: SCALE_LABELS[s] }])
);
}
get showCalendar() {
return !this.env.isSmall || !this.state.showSideBar;
@@ -215,7 +216,6 @@ CalendarController.components = {
MobileFilterPanel: CalendarMobileFilterPanel,
QuickCreate: CalendarQuickCreate,
Layout,
Dropdown,
DropdownItem,
ViewScaleSelector,
};
CalendarController.template = "web.CalendarController";
@@ -48,22 +48,7 @@
<i class="fa fa-arrow-right" />
</button>
</span>
<span class="o_calendar_scale_buttons">
<Dropdown class="'btn-group'" togglerClass="'btn btn-secondary scale_button_selection text-uppercase'" hotkey="'v'" showCaret="true">
<t t-set-slot="toggler">
<t t-esc="scaleLabels[model.scale]" />
</t>
<t t-foreach="model.scales" t-as="scale" t-key="scale">
<DropdownItem
class="`o_calendar_button_${scale} btn btn-secondary text-uppercase`"
hotkey="scale[0]"
onSelected="() => this.setScale(scale)"
>
<t t-esc="scaleLabels[scale]" />
</DropdownItem>
</t>
</Dropdown>
</span>
<ViewScaleSelector scales="scales" currentScale="model.scale" setScale.bind="setScale" />
<button t-if="env.isSmall" class="o_cp_today_button btn btn-sm btn-link" t-on-click="() => this.setDate('today')">
<span class="fa-stack o_calendar_button_today">
<i class="fa fa-calendar-o fa-stack-2x" role="img" aria-label="Today" title="Today" />
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { Component } from "@odoo/owl";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
export class ViewScaleSelector extends Component {
static components = {
Dropdown,
DropdownItem,
};
static template = "web.ViewScaleSelector";
static props = {
scales: { type: Object },
currentScale: { type: String },
setScale: { type: Function },
};
get scales() {
return Object.entries(this.props.scales).map(([key, value]) => ({ key, ...value }));
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.ViewScaleSelector" owl="1">
<Dropdown t-if="Object.keys(props.scales).length > 1" class="'btn-group o_view_scale_selector'" togglerClass="'btn btn-secondary scale_button_selection text-uppercase'" hotkey="'v'" showCaret="true">
<t t-set-slot="toggler">
<t t-esc="props.scales[props.currentScale].description" />
</t>
<t t-foreach="scales" t-as="scale" t-key="scale.key">
<DropdownItem
class="`o_scale_button_${scale.key} text-uppercase ${scale.key === props.currentScale ? 'active' : ''}`"
hotkey="scale.hotkey or scale.key[0]"
onSelected="() => this.props.setScale(scale.key)"
>
<t t-esc="scale.description" />
</DropdownItem>
</t>
</Dropdown>
</t>
</templates>
@@ -94,7 +94,7 @@ QUnit.module("Views", ({ beforeEach }) => {
assert.containsNone(target, ".o_calendar_button_next", "next button should be hidden");
assert.isVisible(
target.querySelector(
".o_cp_bottom_left .o_calendar_buttons .o_calendar_scale_buttons + button.o_cp_today_button"
".o_cp_bottom_left .o_calendar_buttons .o_view_scale_selector + button.o_cp_today_button"
),
"today button should be visible near the calendar buttons (bottom left corner)"
);
@@ -113,7 +113,7 @@ QUnit.module("Views", ({ beforeEach }) => {
// switch to day mode
await click(target, ".o_control_panel .scale_button_selection");
await click(target, ".o_control_panel .o_calendar_button_day");
await click(target, ".o_control_panel .o_scale_button_day");
await nextTick();
assert.containsOnce(
target,
@@ -127,7 +127,7 @@ QUnit.module("Views", ({ beforeEach }) => {
// switch to month mode
await click(target, ".o_control_panel .scale_button_selection");
await click(target, ".o_control_panel .o_calendar_button_month");
await click(target, ".o_control_panel .o_scale_button_month");
await nextTick();
assert.containsOnce(
target,
@@ -141,7 +141,7 @@ QUnit.module("Views", ({ beforeEach }) => {
// switch to year mode
await click(target, ".o_control_panel .scale_button_selection");
await click(target, ".o_control_panel .o_calendar_button_year");
await click(target, ".o_control_panel .o_scale_button_year");
await nextTick();
assert.containsOnce(
target,
@@ -360,7 +360,7 @@ QUnit.module("Views", ({ beforeEach }) => {
"week scale should highlight 7 days in mini calendar"
);
await click(target, ".scale_button_selection");
await click(target, ".o_calendar_button_day"); // display only one day
await click(target, ".o_scale_button_day"); // display only one day
assert.containsN(target, ".fc-event", 2, "should display 2 events on the day");
assert.containsOnce(
target,
@@ -368,7 +368,7 @@ QUnit.module("Views", ({ beforeEach }) => {
"should highlight the target day in mini calendar"
);
await click(target, ".scale_button_selection");
await click(target, ".o_calendar_button_month"); // display all the month
await click(target, ".o_scale_button_month"); // display all the month
// We display the events or partner 1 2 and 4. Partner 2 has nothing and Event 6 is for partner 6 (not displayed)
await click(target, ".o_calendar_filter_item[data-value='all'] input");
await click(
@@ -4337,11 +4337,11 @@ QUnit.module("Views", ({ beforeEach }) => {
`,
});
await click(target, ".o_calendar_scale_buttons .scale_button_selection");
assert.containsOnce(target, ".o_calendar_scale_buttons .o_calendar_button_day");
assert.containsOnce(target, ".o_calendar_scale_buttons .o_calendar_button_week");
assert.containsNone(target, ".o_calendar_scale_buttons .o_calendar_button_month");
assert.containsNone(target, ".o_calendar_scale_buttons .o_calendar_button_year");
await click(target, ".o_view_scale_selector .scale_button_selection");
assert.containsOnce(target, ".o_view_scale_selector .o_scale_button_day");
assert.containsOnce(target, ".o_view_scale_selector .o_scale_button_week");
assert.containsNone(target, ".o_view_scale_selector .o_scale_button_month");
assert.containsNone(target, ".o_view_scale_selector .o_scale_button_year");
});
QUnit.test(`click outside the popup should close it`, async (assert) => {
@@ -498,8 +498,8 @@ export async function resizeEventToTime(target, eventId, dateTime) {
}
export async function changeScale(target, scale) {
await click(target, `.o_calendar_scale_buttons .scale_button_selection`);
await click(target, `.o_calendar_scale_buttons .o_calendar_button_${scale}`);
await click(target, `.o_view_scale_selector .scale_button_selection`);
await click(target, `.o_view_scale_selector .o_scale_button_${scale}`);
await nextTick();
}
@@ -0,0 +1,125 @@
/** @odoo-module **/
import { Component, useState, xml } from "@odoo/owl";
import { ViewScaleSelector } from "@web/views/view_components/view_scale_selector";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { click, getFixture, mount } from "@web/../tests/helpers/utils";
import { registry } from "@web/core/registry";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { uiService } from "@web/core/ui/ui_service";
const serviceRegistry = registry.category("services");
let target;
QUnit.module("ViewComponents", (hooks) => {
hooks.beforeEach(async () => {
target = getFixture();
serviceRegistry.add("ui", uiService);
serviceRegistry.add("hotkey", hotkeyService);
});
QUnit.module("ViewScaleSelector");
QUnit.test("basic ViewScaleSelector component usage", async (assert) => {
const env = await makeTestEnv();
class Parent extends Component {
static components = { ViewScaleSelector };
static template = xml`<ViewScaleSelector t-props="compProps" />`;
setup() {
this.state = useState({
scale: "week",
});
}
get compProps() {
return {
scales: {
day: {
description: "Daily",
},
week: {
description: "Weekly",
hotkey: "o",
},
year: {
description: "Yearly",
},
},
setScale: (scale) => {
this.state.scale = scale;
assert.step(scale);
},
currentScale: this.state.scale,
};
}
}
await mount(Parent, target, { env });
assert.containsOnce(target, ".o_view_scale_selector");
assert.verifySteps([]);
assert.strictEqual(
target.querySelector(".o_view_scale_selector").textContent,
"Weekly",
"toggler displays the right text"
);
assert.strictEqual(
target.querySelector(".scale_button_selection").dataset.hotkey,
"v",
"toggler has the right hotkey"
);
await click(target, ".scale_button_selection");
assert.containsOnce(target, ".o_view_scale_selector .dropdown-menu", "a dropdown appeared");
assert.strictEqual(
target.querySelector(".o_view_scale_selector .dropdown-menu .active").textContent,
"Weekly",
"the active option is selected"
);
assert.strictEqual(
target.querySelector(".o_view_scale_selector .dropdown-menu span:nth-child(2)").dataset
.hotkey,
"o",
"'week' scale has the right hotkey"
);
await click(target, ".o_scale_button_day");
assert.verifySteps(["day"]);
assert.strictEqual(
target.querySelector(".o_view_scale_selector").textContent,
"Daily",
"the right text is displayed on the toggler"
);
});
QUnit.test("ViewScaleSelector with only one scale available", async (assert) => {
const env = await makeTestEnv();
class Parent extends Component {
static components = { ViewScaleSelector };
static template = xml`<ViewScaleSelector t-props="compProps" />`;
setup() {
this.state = useState({
scale: "day",
});
}
get compProps() {
return {
scales: {
day: {
description: "Daily",
},
},
setScale: () => {},
currentScale: this.state.scale,
};
}
}
await mount(Parent, target, { env });
assert.containsNone(
target,
".o_view_scale_selector",
"toggler is not present as no other option is available"
);
});
});