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