[IMP] web,*: make ControlPanel, SearchView, SearchPanel responsive
This commit transfers responsive related customizations for ControlPanel, SearchView and SearchPanel to community which allows them to render properly on smaller devices. Note: a page reload (aka. F5) is required to properly adapt the UI after a resize to a mobile-like size. X-original-commit: 26a755610da36b01ba07ca224cc5937a672eb6a8 Part-of: odoo/odoo#100759
This commit is contained in:
+22
-1
@@ -25,10 +25,31 @@
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="project.ProjectControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
|
||||
<t t-name="project.Breadcrumbs.Small" t-inherit="web.Breadcrumbs.Small" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//ol" position="inside">
|
||||
<t t-call="project.ProjectControlPanelContent"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="project.ProjectControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-call='web.Breadcrumbs']" position="replace">
|
||||
<t t-call="project.Breadcrumbs"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="project.ProjectControlPanel.Small" t-inherit="web.ControlPanel.Small" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-call='web.Breadcrumbs.Small']" position="replace">
|
||||
<t t-call="project.Breadcrumbs.Small"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="project.ProjectControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
|
||||
<t t-call="project.ProjectControlPanel.Regular"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-call='web.ControlPanel.Small']" position="replace">
|
||||
<t t-call="project.ProjectControlPanel.Small"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="stock.StockReportSearchPanel" t-inherit="web.SearchPanel" owl="1">
|
||||
<t t-name="stock.StockReportSearchPanel.Regular" t-inherit="web.SearchPanel.Regular" owl="1">
|
||||
<xpath expr="//div/section[1]" position="before">
|
||||
<section t-if="warehouses.length > 1" class="o_search_panel_section o_search_panel_warehouse">
|
||||
<header class="o_search_panel_section_header pt-4 pb-2 text-uppercase o_cursor_default">
|
||||
@@ -21,4 +21,11 @@
|
||||
</section>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="stock.StockReportSearchPanel" t-inherit="web.SearchPanel" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-call='web.SearchPanel.Regular']" position="replace">
|
||||
<t t-call="stock.StockReportSearchPanel.Regular"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
|
||||
import { Pager } from "@web/core/pager/pager";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
@@ -8,8 +9,9 @@ import { FavoriteMenu } from "../favorite_menu/favorite_menu";
|
||||
import { FilterMenu } from "../filter_menu/filter_menu";
|
||||
import { GroupByMenu } from "../group_by_menu/group_by_menu";
|
||||
import { SearchBar } from "../search_bar/search_bar";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
|
||||
const { Component, useState } = owl;
|
||||
const { Component, useState, onMounted, useExternalListener, useRef, useEffect } = owl;
|
||||
|
||||
const MAPPING = {
|
||||
filter: FilterMenu,
|
||||
@@ -18,24 +20,66 @@ const MAPPING = {
|
||||
favorite: FavoriteMenu,
|
||||
};
|
||||
|
||||
const STICKY_CLASS = "o_mobile_sticky";
|
||||
|
||||
export class ControlPanel extends Component {
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.pagerProps = this.env.config.pagerProps
|
||||
? useState(this.env.config.pagerProps)
|
||||
: undefined;
|
||||
|
||||
this.root = useRef("root");
|
||||
|
||||
this.state = useState({
|
||||
showSearchBar: false,
|
||||
showMobileSearch: false,
|
||||
showViewSwitcher: false,
|
||||
});
|
||||
|
||||
this.onScrollThrottledBound = this.onScrollThrottled.bind(this);
|
||||
|
||||
useExternalListener(window, "click", this.onWindowClick);
|
||||
useEffect(() => {
|
||||
if (
|
||||
!this.env.isSmall ||
|
||||
("adaptToScroll" in this.display && !this.display.adaptToScroll)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const scrollingEl = this.getScrollingElement();
|
||||
scrollingEl.addEventListener("scroll", this.onScrollThrottledBound);
|
||||
this.root.el.style.top = "0px";
|
||||
return () => {
|
||||
scrollingEl.removeEventListener("scroll", this.onScrollThrottledBound);
|
||||
};
|
||||
});
|
||||
onMounted(() => {
|
||||
if (
|
||||
!this.env.isSmall ||
|
||||
("adaptToScroll" in this.display && !this.display.adaptToScroll)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.oldScrollTop = 0;
|
||||
this.lastScrollTop = 0;
|
||||
this.initialScrollTop = this.getScrollingElement().scrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
getScrollingElement() {
|
||||
return this.root.el.parentElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} ev
|
||||
* Reset mobile search state
|
||||
*/
|
||||
onBottomLeftKeydown(ev) {
|
||||
const hotkey = getActiveHotkey(ev);
|
||||
if (hotkey === "arrowdown") {
|
||||
this.env.searchModel.trigger("focus-view");
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
resetSearchState() {
|
||||
Object.assign(this.state, {
|
||||
showSearchBar: false,
|
||||
showMobileSearch: false,
|
||||
showViewSwitcher: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,18 +129,81 @@ export class ControlPanel extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a view is clicked in the view switcher.
|
||||
* Show or hide the control panel on the top screen.
|
||||
* The function is throttled to avoid refreshing the scroll position more
|
||||
* often than necessary.
|
||||
*/
|
||||
onScrollThrottled() {
|
||||
if (this.isScrolling) {
|
||||
return;
|
||||
}
|
||||
this.isScrolling = true;
|
||||
browser.requestAnimationFrame(() => (this.isScrolling = false));
|
||||
|
||||
const scrollTop = this.getScrollingElement().scrollTop;
|
||||
const delta = Math.round(scrollTop - this.oldScrollTop);
|
||||
|
||||
if (scrollTop > this.initialScrollTop) {
|
||||
// Beneath initial position => sticky display
|
||||
this.root.el.classList.add(STICKY_CLASS);
|
||||
if (delta < 0) {
|
||||
// Going up
|
||||
this.lastScrollTop = Math.min(0, this.lastScrollTop - delta);
|
||||
} else {
|
||||
// Going down | not moving
|
||||
this.lastScrollTop = Math.max(
|
||||
-this.root.el.offsetHeight,
|
||||
-this.root.el.offsetTop - delta
|
||||
);
|
||||
}
|
||||
this.root.el.style.top = `${this.lastScrollTop}px`;
|
||||
} else {
|
||||
// Above initial position => standard display
|
||||
this.root.el.classList.remove(STICKY_CLASS);
|
||||
this.lastScrollTop = 0;
|
||||
}
|
||||
|
||||
this.oldScrollTop = scrollTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a view is clicked in the view switcher
|
||||
* and reset mobile search state on switch view.
|
||||
*
|
||||
* @param {ViewType} viewType
|
||||
*/
|
||||
onViewClicked(viewType) {
|
||||
this.resetSearchState();
|
||||
this.actionService.switchView(viewType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onWindowClick(ev) {
|
||||
if (this.state.showViewSwitcher && !ev.target.closest(".o_cp_switch_buttons")) {
|
||||
this.state.showViewSwitcher = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
onBottomLeftKeydown(ev) {
|
||||
const hotkey = getActiveHotkey(ev);
|
||||
if (hotkey === "arrowdown") {
|
||||
this.env.searchModel.trigger("focus-view");
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ControlPanel.components = {
|
||||
...Object.values(MAPPING),
|
||||
Pager,
|
||||
SearchBar,
|
||||
Dropdown,
|
||||
};
|
||||
ControlPanel.template = "web.ControlPanel";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
.o_control_panel {
|
||||
border-bottom: 1px solid $border-color;
|
||||
@include o-webclient-padding($top: 5px, $bottom: 5px);
|
||||
@@ -17,12 +16,111 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
padding-right: 0;
|
||||
width: 100%;
|
||||
|
||||
> div.o_cp_bottom {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
&.o_mobile_sticky {
|
||||
@include o-position-sticky();
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.o_cp_action_menus {
|
||||
.o_dropdown_title {
|
||||
@include visually-hidden;
|
||||
}
|
||||
|
||||
.o_dropdown_chevron, .o_dropdown_caret {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_cp_searchview {
|
||||
min-height: 35px;
|
||||
|
||||
&.o_searchview_quick {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
|
||||
> .o_searchview {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $o-brand-secondary;
|
||||
|
||||
> .o_searchview_input_container {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
> .o_enable_searchview {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
&, &:hover {
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_cp_bottom_left, .o_cp_bottom_right {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.o_cp_switch_buttons {
|
||||
&.show > .dropdown-menu {
|
||||
display: inline-flex;
|
||||
min-width: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 18px;
|
||||
|
||||
> li {
|
||||
@include o-text-overflow($max-width: 90%);
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
> li {
|
||||
margin: 0;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.o_back_button {
|
||||
&, & ~ li {
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
content: ""; // fa-arrow-left
|
||||
display: inline-block;
|
||||
|
||||
padding: 0; // override bootstrap
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.btn {
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
> a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_cp_top_right {
|
||||
@@ -42,6 +140,20 @@
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
display: flex;
|
||||
padding-right: 25px;
|
||||
|
||||
> .btn-group {
|
||||
margin: auto 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_hidden_input_file {
|
||||
position: relative;
|
||||
input.o_input_file {
|
||||
@@ -119,6 +231,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.o_rtl .o_control_panel .o_back_button:before {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.o_control_panel {
|
||||
display: none;
|
||||
|
||||
@@ -3,50 +3,170 @@
|
||||
|
||||
<t t-name="web.ControlPanel" owl="1">
|
||||
<div class="o_control_panel" t-ref="root">
|
||||
<t t-if="env.isSmall">
|
||||
<t t-call="web.ControlPanel.Small" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="web.ControlPanel.Regular" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.ControlPanel.Regular" owl="1">
|
||||
<div t-if="display['top']" class="o_cp_top">
|
||||
<div class="o_cp_top_left">
|
||||
<t t-slot="control-panel-top-left" t-if="display['top-left']">
|
||||
<t t-call="web.Breadcrumbs" t-if="!env.config.noBreadcrumbs"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_cp_top_right">
|
||||
<t t-slot="control-panel-top-right" t-if="display['top-right']">
|
||||
<SearchBar/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="display['bottom']" class="o_cp_bottom">
|
||||
<div class="o_cp_bottom_left" t-on-keydown="onBottomLeftKeydown">
|
||||
<t t-slot="control-panel-bottom-left-buttons" t-if="display['bottom-left'] and display['bottom-left-buttons']" />
|
||||
<t t-slot="control-panel-bottom-left" t-if="display['bottom-left']"/>
|
||||
</div>
|
||||
<div t-if="display['bottom-right']" class="o_cp_bottom_right">
|
||||
<t t-slot="control-panel-bottom-right">
|
||||
<div class="btn-group o_search_options position-static" role="search">
|
||||
<t t-foreach="searchMenus" t-as="menu" t-key="menu.key">
|
||||
<t t-component="menu.Component"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="o_cp_pager" role="search">
|
||||
<Pager t-if="pagerProps and pagerProps.total > 0" t-props="pagerProps"/>
|
||||
</div>
|
||||
|
||||
<t t-if="env.config.viewSwitcherEntries.length > 1">
|
||||
<nav class="btn-group o_cp_switch_buttons">
|
||||
<t t-foreach="env.config.viewSwitcherEntries" t-as="view" t-key="view.type">
|
||||
<button class="btn btn-light o_switch_view "
|
||||
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
|
||||
t-att-data-tooltip="view.name"
|
||||
t-on-click="() => this.onViewClicked(view.type)"
|
||||
/>
|
||||
</t>
|
||||
</nav>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.ControlPanel.Small" owl="1">
|
||||
<t t-if="state.showMobileSearch">
|
||||
<t t-portal="'body'">
|
||||
<div class="o_searchview o_mobile_search">
|
||||
<div class="o_mobile_search_header">
|
||||
<button
|
||||
type="button"
|
||||
class="o_mobile_search_button btn"
|
||||
t-on-click="() => state.showMobileSearch = false"
|
||||
>
|
||||
<i class="fa fa-arrow-left"/>
|
||||
<strong class="ms-2">FILTER</strong>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="o_mobile_search_button btn"
|
||||
t-on-click="() => env.searchModel.clearQuery()"
|
||||
>
|
||||
CLEAR
|
||||
</button>
|
||||
</div>
|
||||
<div class="o_mobile_search_content">
|
||||
<t t-if="display['top-right']">
|
||||
<t t-slot="control-panel-top-right">
|
||||
<SearchBar/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="display['bottom-right']">
|
||||
<t t-slot="control-panel-bottom-right">
|
||||
<div class="o_mobile_search_filter o_search_options">
|
||||
<t t-foreach="searchMenus" t-as="menu" t-key="menu.key">
|
||||
<t t-component="menu.Component"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary o_mobile_search_footer"
|
||||
t-on-click="() => state.showMobileSearch = false"
|
||||
>
|
||||
SEE RESULT
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div t-if="display['top']" class="o_cp_top">
|
||||
<div class="o_cp_top_left">
|
||||
<t t-slot="control-panel-top-left" t-if="display['top-left']">
|
||||
<t t-call="web.Breadcrumbs" t-if="!env.config.noBreadcrumbs"/>
|
||||
<t t-if="display['top-left'] and !state.showSearchBar">
|
||||
<t t-slot="control-panel-top-left">
|
||||
<t t-call="web.Breadcrumbs.Small" />
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_cp_top_right">
|
||||
<t t-slot="control-panel-top-right" t-if="display['top-right']">
|
||||
<SearchBar/>
|
||||
</t>
|
||||
<t t-if="display['top-right']">
|
||||
<button type="button" class="o_enable_searchview btn btn-link"
|
||||
t-att-class="state.showSearchBar ? 'fa fa-arrow-left' : 'oi oi-search'"
|
||||
t-on-click="() => state.showSearchBar = !state.showSearchBar"
|
||||
/>
|
||||
<t t-if="state.showSearchBar or !display['top-left']">
|
||||
<t t-slot="control-panel-top-right">
|
||||
<SearchBar class="o_searchview_quick" />
|
||||
<button
|
||||
type="button"
|
||||
class="o_toggle_searchview_full btn fa fa-filter"
|
||||
t-on-click="() => state.showMobileSearch = !state.showMobileSearch"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="display['bottom']" class="o_cp_bottom">
|
||||
<div class="o_cp_bottom_left" t-on-keydown="onBottomLeftKeydown">
|
||||
<t t-slot="control-panel-bottom-left-buttons" t-if="display['bottom-left'] and display['bottom-left-buttons']" />
|
||||
<t t-slot="control-panel-bottom-left" t-if="display['bottom-left']"/>
|
||||
<div t-if="display['bottom-left']" class="o_cp_bottom_left">
|
||||
<t t-slot="control-panel-bottom-left-buttons" t-if="display['bottom-left-buttons']"/>
|
||||
<t t-slot="control-panel-bottom-left"/>
|
||||
</div>
|
||||
<div t-if="display['bottom-right']" class="o_cp_bottom_right">
|
||||
<t t-slot="control-panel-bottom-right">
|
||||
<div class="btn-group o_search_options position-static" role="search">
|
||||
<t t-foreach="searchMenus" t-as="menu" t-key="menu.key">
|
||||
<t t-component="menu.Component"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="o_cp_pager" role="search">
|
||||
<Pager t-if="pagerProps and pagerProps.total > 0" t-props="pagerProps"/>
|
||||
</div>
|
||||
|
||||
<t t-if="env.config.viewSwitcherEntries.length > 1">
|
||||
<nav class="btn-group o_cp_switch_buttons">
|
||||
<t t-foreach="env.config.viewSwitcherEntries" t-as="view" t-key="view.type">
|
||||
<button class="btn btn-light o_switch_view "
|
||||
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
|
||||
t-att-data-tooltip="view.name"
|
||||
t-on-click="() => this.onViewClicked(view.type)"
|
||||
<t t-set="view" t-value="env.config.viewSwitcherEntries.find((v) => v.active)" />
|
||||
|
||||
<Dropdown
|
||||
position="'bottom-end'"
|
||||
menuClass="'d-inline-flex'"
|
||||
togglerClass="'btn btn-link'"
|
||||
>
|
||||
<t t-set-slot="toggler">
|
||||
<i
|
||||
class="fa-lg o_switch_view"
|
||||
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-foreach="env.config.viewSwitcherEntries" t-as="view" t-key="view.type">
|
||||
<button class="btn btn-light fa-lg o_switch_view"
|
||||
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
|
||||
t-att-data-tooltip="view.name"
|
||||
t-on-click="() => this.onViewClicked(view.type)"
|
||||
/>
|
||||
</t>
|
||||
</Dropdown>
|
||||
</nav>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web.Breadcrumbs" owl="1">
|
||||
@@ -67,4 +187,21 @@
|
||||
</ol>
|
||||
</t>
|
||||
|
||||
<t t-name="web.Breadcrumbs.Small" owl="1">
|
||||
<ol class="breadcrumb">
|
||||
<t t-set="breadcrumbs" t-value="env.config.breadcrumbs" />
|
||||
<t t-if="breadcrumbs.length > 1">
|
||||
<t t-set="breadcrumb" t-value="breadcrumbs[breadcrumbs.length - 2]" />
|
||||
<li class="breadcrumb-item o_back_button btn btn-secondary"
|
||||
t-on-click.prevent="() => this.onBreadcrumbClicked(breadcrumb.jsId)"
|
||||
/>
|
||||
</t>
|
||||
<li t-if="breadcrumbs.length > 0" class="breadcrumb-item active">
|
||||
<t t-set="breadcrumb" t-value="breadcrumbs[breadcrumbs.length - 1]" />
|
||||
<t t-if="breadcrumb.name" t-esc="breadcrumb.name"/>
|
||||
<em t-else="" class="text-warning">Unnamed</em>
|
||||
</li>
|
||||
</ol>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -4,6 +4,27 @@ import { useBus } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, onMounted, onWillStart, useRef, useState } = owl;
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
const isFilter = (s) => s.type === "filter";
|
||||
const isActiveCategory = (s) => s.type === "category" && s.activeValueId;
|
||||
|
||||
/**
|
||||
* @param {Map<string | false, Object>} values
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
const nameOfCheckedValues = (values) => {
|
||||
const names = [];
|
||||
for (const [, value] of values) {
|
||||
if (value.checked) {
|
||||
names.push(value.display_name);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
};
|
||||
|
||||
/**
|
||||
* Search panel
|
||||
*
|
||||
@@ -18,6 +39,7 @@ export class SearchPanel extends Component {
|
||||
this.state = useState({
|
||||
active: {},
|
||||
expanded: {},
|
||||
showMobileSearch: false,
|
||||
});
|
||||
this.root = useRef("root");
|
||||
this.scrollTop = 0;
|
||||
@@ -108,6 +130,52 @@ export class SearchPanel extends Component {
|
||||
return parentId ? [...this.getAncestorValueIds(category, parentId), parentId] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted version of the active categories to populate
|
||||
* the selection banner of the control panel summary.
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
getCategorySelection() {
|
||||
const activeCategories = this.env.searchModel.getSections(isActiveCategory);
|
||||
const selection = [];
|
||||
for (const category of activeCategories) {
|
||||
const parentIds = this.getAncestorValueIds(category, category.activeValueId);
|
||||
const orderedCategoryNames = [...parentIds, category.activeValueId].map(
|
||||
(valueId) => category.values.get(valueId).display_name
|
||||
);
|
||||
selection.push({
|
||||
values: orderedCategoryNames,
|
||||
icon: category.icon,
|
||||
color: category.color,
|
||||
});
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted version of the active filters to populate
|
||||
* the selection banner of the control panel summary.
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
getFilterSelection() {
|
||||
const filters = this.env.searchModel.getSections(isFilter);
|
||||
const selection = [];
|
||||
for (const { groups, values, icon, color } of filters) {
|
||||
let filterValues;
|
||||
if (groups) {
|
||||
filterValues = Object.keys(groups)
|
||||
.map((groupId) => nameOfCheckedValues(groups[groupId].values))
|
||||
.flat();
|
||||
} else if (values) {
|
||||
filterValues = nameOfCheckedValues(values);
|
||||
}
|
||||
if (filterValues.length) {
|
||||
selection.push({ values: filterValues, icon, color });
|
||||
}
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent unnecessary calls to the model by ensuring a different category
|
||||
* is clicked.
|
||||
|
||||
@@ -37,3 +37,20 @@
|
||||
width: map-get($spacers, 4);
|
||||
}
|
||||
}
|
||||
|
||||
.o_mobile_search_content {
|
||||
--SearchPanel-width: 100%;
|
||||
--SearchPanel-fontSize: 1.1em;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.o_component_with_search_panel,
|
||||
.o_controller_with_searchpanel {
|
||||
flex-direction: column;
|
||||
align-items: initial;
|
||||
|
||||
.o_renderer_with_searchpanel.o_list_view {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,25 @@
|
||||
|
||||
$o-search-panel-width: 200px;
|
||||
$o-search-panel-font-size: 1em;
|
||||
|
||||
@mixin o-details-modal($top: 0, $bottom: 0) {
|
||||
position: fixed;
|
||||
z-index: $zindex-modal;
|
||||
right: 0;
|
||||
top: $top;
|
||||
bottom: $bottom;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@mixin o-details-modal-header {
|
||||
padding: 0.7rem 1.4rem;
|
||||
height: $o-navbar-height;
|
||||
}
|
||||
|
||||
@mixin o-details-hide-caret {
|
||||
// Hide the caret. For details see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary
|
||||
list-style-type: none;
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="web.SearchPanel" owl="1">
|
||||
<t t-if="env.isSmall">
|
||||
<t t-call="web.SearchPanel.Small" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="web.SearchPanel.Regular" />
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web.SearchPanelContent" owl="1">
|
||||
<div class="o_search_panel flex-grow-0 flex-shrink-0 border-end pe-2 pb-5 ps-4 h-100 bg-white overflow-auto" t-att-class="env.searchModel.searchPanelInfo.className" t-ref="root">
|
||||
<section t-foreach="sections" t-as="section" t-key="section.id"
|
||||
t-attf-class="o_search_panel_section o_search_panel_{{ section.type }}"
|
||||
@@ -31,7 +40,7 @@
|
||||
<header class="o_search_panel_group_header pb-1">
|
||||
<div class="form-check w-100">
|
||||
<!-- TODO: "indeterminate" could not be set in the template and had to be set in
|
||||
JS manually. See https://github.com/odoo/owl/issues/713 (adapt when solved)
|
||||
JS manually. See https://github.com/odoo/owl/issues/713 (adapt when solved)
|
||||
-->
|
||||
<input type="checkbox"
|
||||
class="form-check-input"
|
||||
@@ -77,6 +86,72 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.SearchPanel.Regular" t-inherit="web.SearchPanelContent" t-inherit-mode="primary" owl="1"/>
|
||||
|
||||
<t t-name="web.SearchPanel.Small" owl="1">
|
||||
<t t-if="state.showMobileSearch">
|
||||
<t t-portal="'body'">
|
||||
<div class="o_search_panel o_searchview o_mobile_search" t-ref="root">
|
||||
<div class="o_mobile_search_header">
|
||||
<button type="button"
|
||||
class="o_mobile_search_button btn"
|
||||
t-on-click="() => state.showMobileSearch = false"
|
||||
>
|
||||
<i class="fa fa-arrow-left" />
|
||||
<strong class="ms-2">FILTER</strong>
|
||||
</button>
|
||||
</div>
|
||||
<div class="o_mobile_search_content">
|
||||
<t t-call="web.SearchPanelContent" />
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-primary o_mobile_search_footer"
|
||||
t-on-click.stop="() => state.showMobileSearch = false"
|
||||
>
|
||||
<t>SEE RESULT</t>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<!-- Summary header -->
|
||||
<t t-else="">
|
||||
<button
|
||||
class="o_search_panel o_search_panel_summary btn w-100 overflow-visible"
|
||||
t-on-click="() => state.showMobileSearch = true"
|
||||
t-ref="root"
|
||||
>
|
||||
<t t-set="categories" t-value="getCategorySelection()" />
|
||||
<t t-set="filters" t-value="getFilterSelection()" />
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa fa-fw fa-filter" />
|
||||
<div class="o_search_panel_current_selection text-truncate ms-2 me-auto">
|
||||
<t t-if="!categories.length and !filters.length">All</t>
|
||||
<t t-else="">
|
||||
<t t-foreach="categories" t-as="category" t-key="category.id">
|
||||
<span class="o_search_panel_category me-1">
|
||||
<i t-if="category.icon"
|
||||
t-attf-class="o_search_panel_section_icon fa {{ category.icon }} me-1"
|
||||
t-att-style="category.color and ('color: ' + category.color)"
|
||||
/>
|
||||
<t t-esc="category.values.join('/')" />
|
||||
</span>
|
||||
</t>
|
||||
<t t-foreach="filters" t-as="filter" t-key="filter.id">
|
||||
<span class="o_search_panel_filter me-1">
|
||||
<i t-if="filter.icon"
|
||||
t-attf-class="o_search_panel_section_icon fa {{ filter.icon }} me-1"
|
||||
t-att-style="filter.color and ('color: ' + filter.color)"
|
||||
/>
|
||||
<t t-esc="filter.values.join(', ')" />
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web.SearchPanel.Category" owl="1">
|
||||
<t t-foreach="values" t-as="valueId" t-key="valueId">
|
||||
<t t-set="value" t-value="section.values.get(valueId)"/>
|
||||
|
||||
@@ -3,78 +3,107 @@
|
||||
padding: 0 20px 1px 0;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding-bottom: 3px;
|
||||
|
||||
&:focus-within {
|
||||
border-bottom-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-right: 5px; // o_switch_view_button_icon alignment
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.o_searchview_input_container {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.o_searchview_facet {
|
||||
.o_searchview_facet {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
margin: 1px 3px 0 0;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
$o-searchview-facet-remove-width: 18px;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.o_searchview_facet_label {
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0 3px;
|
||||
@include o-text-overflow($display: flex);
|
||||
}
|
||||
|
||||
.o_facet_values {
|
||||
direction: ltr#{"/*rtl:ignore*/"};
|
||||
padding: 0 $o-searchview-facet-remove-width 0 5px;
|
||||
|
||||
.o_facet_values_sep {
|
||||
font-style: italic;
|
||||
margin: 0 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.o_facet_remove {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
margin: 1px 3px 0 0;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
$o-searchview-facet-remove-width: 18px;
|
||||
|
||||
.o_searchview_facet_label {
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0 3px;
|
||||
@include o-text-overflow($display: flex);
|
||||
}
|
||||
|
||||
.o_facet_values {
|
||||
direction: ltr#{"/*rtl:ignore*/"};
|
||||
padding: 0 $o-searchview-facet-remove-width 0 5px;
|
||||
|
||||
.o_facet_values_sep {
|
||||
font-style: italic;
|
||||
margin: 0 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.o_facet_remove {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
justify-content: center;
|
||||
width: $o-searchview-facet-remove-width;
|
||||
@include o-position-absolute(0, 0, 0);
|
||||
}
|
||||
justify-content: center;
|
||||
width: $o-searchview-facet-remove-width;
|
||||
@include o-position-absolute(0, 0, 0);
|
||||
@include o-hover-text-color($text-muted, $o-brand-odoo);
|
||||
}
|
||||
}
|
||||
|
||||
.o_searchview_input {
|
||||
flex: 1 0 auto;
|
||||
width: 75px;
|
||||
}
|
||||
.o_searchview_input {
|
||||
flex: 1 0 auto;
|
||||
width: 75px;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.o_searchview_autocomplete {
|
||||
width: 100%;
|
||||
@include o-position-absolute(100%, $left: auto);
|
||||
.o_searchview_autocomplete {
|
||||
width: 100%;
|
||||
@include o-position-absolute(100%, $left: auto);
|
||||
|
||||
.o_menu_item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding-left: 25px;
|
||||
.o_menu_item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding-left: 25px;
|
||||
|
||||
&.o_indent {
|
||||
padding-left: 50px;
|
||||
}
|
||||
&.o_indent {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
a {
|
||||
color: inherit;
|
||||
|
||||
&.o_expand {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 25px;
|
||||
@include o-position-absolute($left: 0);
|
||||
}
|
||||
&.o_expand {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 25px;
|
||||
@include o-position-absolute($left: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_searchview_icon {
|
||||
@include o-position-absolute($top: 4px, $right: 0);
|
||||
}
|
||||
}
|
||||
|
||||
.o_search_options {
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
// Filter menu
|
||||
@@ -115,3 +144,110 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_web_client .o_mobile_search {
|
||||
align-items: normal;
|
||||
background-color: white;
|
||||
border: none;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: $zindex-modal;
|
||||
|
||||
.o_mobile_search_header {
|
||||
background-color: $o-brand-odoo;
|
||||
display: flex;
|
||||
min-height: $o-navbar-height;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.o_mobile_search_button {
|
||||
color: white;
|
||||
|
||||
&:active {
|
||||
background-color: darken($o-brand-primary, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_mobile_search_content {
|
||||
flex: auto;
|
||||
height: auto;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
|
||||
.o_searchview_input_container {
|
||||
display: flex;
|
||||
padding: 15px 20px 0 20px;
|
||||
position: relative;
|
||||
|
||||
.o_searchview_input {
|
||||
border-bottom: 1px solid $o-brand-secondary;
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.o_searchview_facet {
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
order: 1;
|
||||
|
||||
.o_searchview_facet_label {
|
||||
border-radius: 2em 0em 0em 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.o_searchview_autocomplete {
|
||||
top: 100%;
|
||||
|
||||
> li {
|
||||
margin: 5px 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_mobile_search_filter {
|
||||
padding: 8px 20px 15%;
|
||||
flex: auto;
|
||||
|
||||
.dropdown {
|
||||
margin-top: 15px;
|
||||
flex-direction: column;
|
||||
line-height: 2;
|
||||
|
||||
&:not(.show) {
|
||||
box-shadow: 0 0 0 1px $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown, .dropdown-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// We disable the backdrop in this case because it prevents any
|
||||
// interaction outside of a dropdown while it is open.
|
||||
.dropdown-backdrop {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
// Here we use !important because of popper js adding custom style
|
||||
// to element so to override it use !important
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_mobile_search_footer {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
.o_searchview {
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: $border-radius;
|
||||
padding: 1px 25px 3px 5px;
|
||||
|
||||
.o_searchview_facet {
|
||||
background: $o-brand-lightsecondary;
|
||||
|
||||
.o_facet_remove {
|
||||
color: map-get($grays, '600');
|
||||
&:hover {
|
||||
color: map-get($grays, '700');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_searchview_input {
|
||||
border: none;
|
||||
padding: 4px 0 0 0;
|
||||
}
|
||||
.o_searchview_icon {
|
||||
@include o-position-absolute($top: 6px, $right: 5px);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture, triggerEvent, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import {
|
||||
editSearch,
|
||||
makeWithSearch,
|
||||
setupControlPanelServiceRegistry,
|
||||
} from "@web/../tests/search/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Search", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
setupControlPanelServiceRegistry();
|
||||
target = getFixture();
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true },
|
||||
},
|
||||
records: [{ date_field: "2022-02-14" }],
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"foo,false,search": `
|
||||
<search>
|
||||
<filter name="birthday" date="birthday"/>
|
||||
<filter name="date_field" date="date_field"/>
|
||||
</search>
|
||||
`,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.module("Control Panel (mobile)");
|
||||
|
||||
QUnit.test("Display control panel mobile", async (assert) => {
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewId: false,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".breadcrumb");
|
||||
assert.containsOnce(target, ".o_enable_searchview");
|
||||
assert.containsNone(target, ".o_searchview");
|
||||
assert.containsNone(target, ".o_toggle_searchview_full");
|
||||
|
||||
await click(target, ".o_enable_searchview");
|
||||
|
||||
assert.containsNone(target, ".breadcrumb");
|
||||
assert.containsOnce(target, ".o_enable_searchview");
|
||||
assert.containsOnce(target, ".o_searchview");
|
||||
assert.containsOnce(target, ".o_toggle_searchview_full");
|
||||
|
||||
await click(target, ".o_toggle_searchview_full");
|
||||
|
||||
assert.containsOnce(document.body, ".o_searchview.o_mobile_search");
|
||||
assert.containsN(document.body, ".o_mobile_search .o_mobile_search_button", 2);
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".o_mobile_search_header").textContent.trim(),
|
||||
"FILTER CLEAR"
|
||||
);
|
||||
assert.containsOnce(document.body, ".o_searchview.o_mobile_search .o_cp_searchview");
|
||||
assert.containsOnce(document.body, ".o_searchview.o_mobile_search .o_mobile_search_footer");
|
||||
|
||||
await click(document.body.querySelector(".o_mobile_search_button"));
|
||||
|
||||
assert.containsNone(target, ".breadcrumb");
|
||||
assert.containsOnce(target, ".o_enable_searchview");
|
||||
assert.containsOnce(target, ".o_searchview");
|
||||
assert.containsOnce(target, ".o_toggle_searchview_full");
|
||||
|
||||
await click(target, ".o_enable_searchview");
|
||||
|
||||
assert.containsOnce(target, ".breadcrumb");
|
||||
assert.containsOnce(target, ".o_enable_searchview");
|
||||
assert.containsNone(target, ".o_searchview");
|
||||
assert.containsNone(target, ".o_toggle_searchview_full");
|
||||
});
|
||||
|
||||
QUnit.test("Make a simple search in mobile mode", async (assert) => {
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewFields: {
|
||||
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
|
||||
},
|
||||
searchViewArch: `
|
||||
<search>
|
||||
<field name="birthday"/>
|
||||
</search>
|
||||
`,
|
||||
});
|
||||
assert.containsNone(target, ".o_searchview");
|
||||
|
||||
await click(target, ".o_enable_searchview");
|
||||
assert.containsOnce(target, ".o_searchview");
|
||||
const input = target.querySelector(".o_searchview input");
|
||||
assert.containsNone(target, ".o_searchview_autocomplete");
|
||||
|
||||
await editSearch(target, "2022-02-14");
|
||||
assert.strictEqual(input.value, "2022-02-14", "input value should be updated");
|
||||
assert.containsOnce(target, ".o_searchview_autocomplete");
|
||||
|
||||
await triggerEvent(input, null, "keydown", { key: "Escape" });
|
||||
assert.containsNone(target, ".o_searchview_autocomplete");
|
||||
|
||||
await click(target, ".o_enable_searchview");
|
||||
assert.containsNone(target, ".o_searchview");
|
||||
});
|
||||
|
||||
QUnit.test("Control panel is shown/hide on top when scrolling", async (assert) => {
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
const contentHeight = 200;
|
||||
const sampleContent = document.createElement("div");
|
||||
sampleContent.style.minHeight = `${2 * contentHeight}px`;
|
||||
target.appendChild(sampleContent);
|
||||
const { maxHeight, overflow } = target.style;
|
||||
target.style.maxHeight = `${contentHeight}px`;
|
||||
target.style.overflow = "auto";
|
||||
target.scrollTo({ top: 50 });
|
||||
await nextTick();
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_control_panel"),
|
||||
"o_mobile_sticky",
|
||||
"control panel becomes sticky when the target is not on top"
|
||||
);
|
||||
target.scrollTo({ top: -50 });
|
||||
await nextTick();
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_control_panel"),
|
||||
"o_mobile_sticky",
|
||||
"control panel is not sticky anymore"
|
||||
);
|
||||
target.style.maxHeight = maxHeight;
|
||||
target.style.overflow = overflow;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { SearchPanel } from "@web/search/search_panel/search_panel";
|
||||
import { makeWithSearch, setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
|
||||
const { Component, xml } = owl;
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Search", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
setupControlPanelServiceRegistry();
|
||||
target = getFixture();
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
tag_id: {
|
||||
string: "Many2One",
|
||||
type: "many2one",
|
||||
relation: "tag",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, tag_id: 2 },
|
||||
{ id: 2, tag_id: 1 },
|
||||
{ id: 3, tag_id: 1 },
|
||||
],
|
||||
},
|
||||
tag: {
|
||||
fields: {
|
||||
name: { string: "Name", type: "string" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Gold" },
|
||||
{ id: 2, name: "Silver" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.module("Search Panel (mobile)");
|
||||
|
||||
QUnit.test("basic search panel rendering", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { SearchPanel };
|
||||
Parent.template = xml`<SearchPanel/>`;
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: Parent,
|
||||
searchViewFields: serverData.models.foo.fields,
|
||||
searchViewArch: `
|
||||
<search>
|
||||
<searchpanel>
|
||||
<field name="tag_id" icon="fa-bars" string="Tags"/>
|
||||
</searchpanel>
|
||||
</search>`,
|
||||
});
|
||||
assert.containsOnce(target, ".o_search_panel.o_search_panel_summary");
|
||||
|
||||
await click(target, ".o_search_panel .o_search_panel_current_selection");
|
||||
assert.containsOnce(document.body, ".o_search_panel.o_mobile_search");
|
||||
assert.containsN(document.body, ".o_search_panel_category_value", 3); // All, Gold, Silver
|
||||
});
|
||||
});
|
||||
@@ -33,10 +33,16 @@
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="website.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="extension" owl="1">
|
||||
<t t-name="website.ControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('o_search_options')]/*[last()]" position="after">
|
||||
<t t-slot="control-panel-website-extra-actions"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="website.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
|
||||
<t t-call="website.ControlPanel.Regular"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.ThemePreviewForm.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
|
||||
<t t-name="website.ThemePreviewForm.ControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[hasclass('o_cp_bottom_right')]" position="replace">
|
||||
<div class="o_cp_bottom_right" role="group">
|
||||
<input class="btn-check" id="themeViewerDesktop" type="radio" name="viewer" data-mode='desktop' autocomplete="off" checked='checked'/>
|
||||
@@ -27,7 +27,13 @@
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="website.ThemePreviewKanban.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
|
||||
<t t-name="website.ThemePreviewForm.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
|
||||
<t t-call="website.ThemePreviewForm.ControlPanel.Regular"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="website.ThemePreviewKanban.ControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[hasclass('o_cp_top_left')]" position="replace">
|
||||
<div class="o_cp_top_left">
|
||||
<ol class="breadcrumb" role="navigation">
|
||||
@@ -47,4 +53,10 @@
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="website.ThemePreviewKanban.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
|
||||
<t t-call="website.ThemePreviewKanban.ControlPanel.Regular"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
Reference in New Issue
Block a user