[MIG] web_responsive: Migration to 17.0

This commit is contained in:
Taras Shabaranskyi
2023-11-17 03:43:53 +02:00
parent 08b8d7bfab
commit 7d4e65dac6
89 changed files with 4140 additions and 2311 deletions

View File

@@ -2,64 +2,44 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {Component, useState} from "@odoo/owl";
import {session} from "@web/session";
import {useBus, useService} from "@web/core/utils/hooks";
import {AppMenuItem} from "@web_responsive/components/apps_menu_item/apps_menu_item.esm";
import {AppsMenuSearchBar} from "@web_responsive/components/menu_searchbar/searchbar.esm";
import {NavBar} from "@web/webclient/navbar/navbar";
import {useAutofocus, useBus, useService} from "@web/core/utils/hooks";
import {useHotkey} from "@web/core/hotkeys/hotkey_hook";
import {scrollTo} from "@web/core/utils/scrolling";
import {debounce} from "@web/core/utils/timing";
import {fuzzyLookup} from "@web/core/utils/search";
import {WebClient} from "@web/webclient/webclient";
import {patch} from "web.utils";
import {escapeRegExp} from "@web/core/utils/strings";
const {Component, useState, onPatched, onWillPatch} = owl;
import {patch} from "@web/core/utils/patch";
import {useHotkey} from "@web/core/hotkeys/hotkey_hook";
// Patch WebClient to show AppsMenu instead of default app
patch(WebClient.prototype, "web_responsive.DefaultAppsMenu", {
patch(WebClient.prototype, {
setup() {
this._super();
super.setup();
useBus(this.env.bus, "APPS_MENU:STATE_CHANGED", ({detail: state}) => {
document.body.classList.toggle("o_apps_menu_opened", state);
});
},
});
/**
* @extends Dropdown
*/
export class AppsMenu extends Component {
setup() {
super.setup();
this.state = useState({open: false});
this.theme = session.apps_menu.theme || "milk";
this.menuService = useService("menu");
useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", () => {
this.setOpenState(false, false);
this.setOpenState(false);
});
this._setupKeyNavigation();
}
setOpenState(open_state, from_home_menu_click) {
setOpenState(open_state) {
this.state.open = open_state;
// Load home page with proper systray when opening it from website
if (from_home_menu_click) {
var currentapp = this.menuService.getCurrentApp();
if (currentapp && currentapp.name == "Website") {
if (window.location.pathname != "/web") {
const icon = $(
document.querySelector(".o_navbar_apps_menu button > i")
);
icon.removeClass("fa fa-th-large").append(
$("<span/>", {class: "fa fa-spin fa-spinner"})
);
}
window.location.href = "/web#home";
} else {
this.env.bus.trigger("APPS_MENU:STATE_CHANGED", open_state);
}
} else {
this.env.bus.trigger("APPS_MENU:STATE_CHANGED", open_state);
}
this.env.bus.trigger("APPS_MENU:STATE_CHANGED", open_state);
}
/**
@@ -103,18 +83,18 @@ export class AppsMenu extends Component {
}
_onWindowKeydown(direction) {
const focusableInputElements = document.querySelectorAll(`.o_app`);
const focusableInputElements = document.querySelectorAll(".o-app-menu-item");
if (focusableInputElements.length) {
const focusable = [...focusableInputElements];
const index = focusable.indexOf(document.activeElement);
let nextIndex = 0;
if (direction == "prev" && index >= 0) {
if (direction === "prev" && index >= 0) {
if (index > 0) {
nextIndex = index - 1;
} else {
nextIndex = focusable.length - 1;
}
} else if (direction == "next") {
} else if (direction === "next") {
if (index + 1 < focusable.length) {
nextIndex = index + 1;
} else {
@@ -124,212 +104,20 @@ export class AppsMenu extends Component {
focusableInputElements[nextIndex].focus();
}
}
}
/**
* Reduce menu data to a searchable format understandable by fuzzyLookup
*
* `menuService.getMenuAsTree()` returns array in a format similar to this (only
* relevant data is shown):
*
* ```js
* // This is a menu entry:
* {
* actionID: 12, // Or `false`
* name: "Actions",
* childrenTree: {0: {...}, 1: {...}}}, // List of inner menu entries
* // in the same format or `undefined`
* }
* ```
*
* This format is very hard to process to search matches, and it would
* slow down the search algorithm, so we reduce it with this method to be
* able to later implement a simpler search.
*
* @param {Object} memo
* Reference to current result object, passed on recursive calls.
*
* @param {Object} menu
* A menu entry, as described above.
*
* @returns {Object}
* Reduced object, without entries that have no action, and with a
* format like this:
*
* ```js
* {
* "Discuss": {Menu entry Object},
* "Settings": {Menu entry Object},
* "Settings/Technical/Actions/Actions": {Menu entry Object},
* ...
* }
* ```
*/
function findNames(memo, menu) {
if (menu.actionID) {
var result = "";
if (menu.webIconData) {
const prefix = menu.webIconData.startsWith("P")
? "data:image/svg+xml;base64,"
: "data:image/png;base64,";
result = menu.webIconData.startsWith("data:image")
? menu.webIconData
: prefix + menu.webIconData.replace(/\s/g, "");
}
menu.webIconData = result;
memo[menu.name.trim()] = menu;
}
if (menu.childrenTree) {
const innerMemo = _.reduce(menu.childrenTree, findNames, {});
for (const innerKey in innerMemo) {
memo[menu.name.trim() + " / " + innerKey] = innerMemo[innerKey];
}
}
return memo;
}
/**
* @extends Component
*/
export class AppsMenuSearchBar extends Component {
setup() {
super.setup();
this.state = useState({
results: [],
offset: 0,
hasResults: false,
});
this.searchBarInput = useAutofocus({refName: "SearchBarInput"});
this._searchMenus = debounce(this._searchMenus, 100);
// Store menu data in a format searchable by fuzzy.js
this._searchableMenus = [];
this.menuService = useService("menu");
for (const menu of this.menuService.getApps()) {
Object.assign(
this._searchableMenus,
_.reduce([this.menuService.getMenuAsTree(menu.id)], findNames, {})
);
}
// Set up key navigation
this._setupKeyNavigation();
onWillPatch(() => {
// Allow looping on results
if (this.state.offset < 0) {
this.state.offset = this.state.results.length + this.state.offset;
} else if (this.state.offset >= this.state.results.length) {
this.state.offset -= this.state.results.length;
}
});
onPatched(() => {
// Scroll to selected element on keyboard navigation
if (this.state.results.length) {
const listElement = document.querySelector(".search-results");
const activeElement = listElement.querySelector(".highlight");
if (activeElement) {
scrollTo(activeElement, listElement);
}
}
});
}
/**
* Search among available menu items, and render that search.
*/
_searchMenus() {
const query = this.searchBarInput.el.value;
this.state.hasResults = query !== "";
this.state.results = this.state.hasResults
? fuzzyLookup(query, _.keys(this._searchableMenus), (k) => k)
: [];
}
/**
* Get menu object for a given key.
* @param {String} key Full path to requested menu.
* @returns {Object} Menu object.
*/
_menuInfo(key) {
return this._searchableMenus[key];
}
/**
* Setup navigation among search results
*/
_setupKeyNavigation() {
useHotkey("Home", () => {
this.state.offset = 0;
});
useHotkey("End", () => {
this.state.offset = this.state.results.length - 1;
});
}
_onKeyDown(ev) {
if (ev.code === "Escape") {
ev.stopPropagation();
ev.preventDefault();
const query = this.searchBarInput.el.value;
if (query) {
this.searchBarInput.el.value = "";
this.state.results = [];
this.state.hasResults = false;
} else {
this.env.bus.trigger("ACTION_MANAGER:UI-UPDATED");
}
} else if (ev.code === "Tab") {
if (document.querySelector(".search-results")) {
ev.preventDefault();
if (ev.shiftKey) {
this.state.offset--;
} else {
this.state.offset++;
}
}
} else if (ev.code === "ArrowUp") {
if (document.querySelector(".search-results")) {
ev.preventDefault();
this.state.offset--;
}
} else if (ev.code === "ArrowDown") {
if (document.querySelector(".search-results")) {
ev.preventDefault();
this.state.offset++;
}
} else if (ev.code === "Enter") {
if (this.state.results.length) {
ev.preventDefault();
document.querySelector(".search-results .highlight").click();
}
}
}
_splitName(name) {
const searchValue = this.searchBarInput.el.value;
if (name) {
const splitName = name.split(
new RegExp(`(${escapeRegExp(searchValue)})`, "ig")
);
return searchValue.length && splitName.length > 1 ? splitName : [name];
}
return [];
onMenuClick() {
this.setOpenState(!this.state.open);
}
}
// Patch Navbar to add proper icon for apps
patch(NavBar.prototype, "web_responsive.navbar", {
getWebIconData(menu) {
var result = "/web_responsive/static/img/default_icon_app.png";
if (menu.webIconData) {
const prefix = menu.webIconData.startsWith("P")
? "data:image/svg+xml;base64,"
: "data:image/png;base64,";
result = menu.webIconData.startsWith("data:image")
? menu.webIconData
: prefix + menu.webIconData.replace(/\s/g, "");
}
return result;
Object.assign(AppsMenu, {
template: "web_responsive.AppsMenu",
props: {
slots: {
type: Object,
optional: true,
},
},
});
AppsMenu.template = "web_responsive.AppsMenu";
AppsMenuSearchBar.template = "web_responsive.AppsMenuSearchResults";
Object.assign(NavBar.components, {AppsMenu, AppsMenuSearchBar});
Object.assign(NavBar.components, {AppsMenu, AppMenuItem, AppsMenuSearchBar});

View File

@@ -1,12 +1,34 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
:root {
.o_grid_apps_menu[data-theme="milk"] {
--app-menu-background: url("../../img/home-menu-bg-overlay.svg"),
linear-gradient(
to bottom,
#{$app-menu-background-color},
#{desaturate(lighten($app-menu-background-color, 20%), 15)}
);
}
.o_grid_apps_menu[data-theme="community"] {
--app-menu-background: url("../../img/home-menu-bg-overlay.svg"),
linear-gradient(
to bottom,
#{$o-brand-primary},
#{desaturate(lighten($o-brand-primary, 20%), 15)}
);
}
}
@mixin full-screen-dropdown {
border: none;
box-shadow: none;
min-height: calc(100vh - #{$o-navbar-height});
min-height: calc(var(--vh100, 100vh) - #{$o-navbar-height});
height: 100%;
max-height: calc(var(--vh100, 100vh) - #{$o-navbar-height});
max-height: calc(100dvh - #{$o-navbar-height});
position: fixed;
margin: 0;
width: 100vw;
@@ -21,185 +43,67 @@
}
}
// hide and save odoo default QUnit tests
.o_navbar_apps_menu.hide .dropdown-toggle {
position: absolute !important;
z-index: -100 !important;
}
// Iconized full screen apps menu
.o_navbar_apps_menu {
.fade-enter-active,
.fade-leave-active {
transition: opacity 100ms ease;
.o_grid_apps_menu {
&__button {
background: unset;
border: unset;
outline: unset;
margin-right: 0.25rem;
min-height: $o-navbar-height;
height: $o-navbar-height;
width: $o-navbar-height;
color: $o-navbar-brand-color;
&:hover,
&:focus {
background: $o-navbar-entry-bg--hover;
}
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.dropdown-menu-custom {
@include full-screen-dropdown();
cursor: pointer;
background: url("../../img/home-menu-bg-overlay.svg"),
linear-gradient(
to bottom,
$o-brand-odoo,
desaturate(lighten($o-brand-odoo, 20%), 15)
);
background-size: cover;
border-radius: 0;
// Display apps in a grid
align-content: flex-start;
display: flex !important;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
@include media-breakpoint-up(lg) {
padding: {
left: calc((100vw - 850px) / 2);
right: calc((100vw - 850px) / 2);
}
}
.o-app-menu-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
width: 100%;
gap: 0.25rem;
.dropdown-item {
padding: 0;
}
.o_app {
outline: 0;
height: 100%;
display: flex;
align-items: center;
text-align: center;
flex-direction: column;
justify-content: flex-start;
white-space: normal;
color: $white !important;
padding: 15px 0 10px;
font-size: 1.25rem;
text-shadow: 1px 1px 1px rgba($black, 0.4);
border-radius: 4px;
transition: 300ms ease;
transition-property: background-color;
&:focus {
background-color: rgba($white, 0.05) !important;
}
img {
box-shadow: none;
margin-bottom: 5px;
transition: 300ms ease;
transition-property: box-shadow, transform;
}
&:hover img,
a:focus img {
transform: translateY(-3px);
box-shadow: 0 9px 12px -4px rgba($black, 0.3);
}
// Size depends on screen
width: 33.33333333%;
@include media-breakpoint-up(sm) {
width: 25%;
}
@include media-breakpoint-up(md) {
width: 16.6666666%;
}
}
// Hide app icons when searching
.has-results ~ .o_app {
display: none;
}
.o-app-icon {
height: auto;
max-width: 6rem;
padding: 0;
}
// Search input for menus
.form-row {
width: 100%;
}
.search-container {
width: 100%;
margin: 1rem 1.5rem 0;
.search-input {
display: flex;
justify-items: center;
box-shadow: inset 0 1px 0 rgba($white, 0.1), 0 1px 0 rgba($black, 0.1);
text-shadow: 0 1px 0 rgba($black, 0.5);
border-radius: 4px;
padding: 0.4rem 0.8rem;
margin-bottom: 1rem;
background-color: rgba($white, 0.1);
@include media-breakpoint-up(md) {
padding: 0.8rem 1.2rem;
}
.search-icon {
color: $white;
font-size: 1.5rem;
margin-right: 1rem;
padding-top: 1px;
}
.form-control {
height: 2rem;
background: none;
border: none;
color: $white;
display: block;
padding: 1px 2px 2px 2px;
box-shadow: none;
&::placeholder {
color: $white;
opacity: 0.5;
}
}
}
// Allow to scroll only on results, keeping static search box above
.search-results {
.text-ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.text-primary {
color: red !important;
}
margin-top: 1rem;
max-height: calc(100vh - #{$o-navbar-height} - 8rem) !important;
overflow: auto;
position: relative;
}
.search-result {
display: block;
align-items: center;
background-position: left;
background-repeat: no-repeat;
background-size: contain;
color: $white;
cursor: pointer;
line-height: 2.5rem;
padding-left: 3.5rem;
white-space: normal;
font-weight: 100;
&.highlight,
&:hover {
background-color: rgba($black, 0.11);
}
b {
font-weight: 700;
}
}
@include media-breakpoint-up(sm) {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
}
}
}
.dropdown-menu-custom {
max-height: 70vh;
.app-menu-container {
@include full-screen-dropdown();
overflow: auto;
background-clip: border-box;
box-shadow: $o-dropdown-box-shadow;
padding: 1rem 0.5rem;
gap: 1rem;
background: var(--app-menu-background);
background-size: cover;
border-radius: 0;
// Display apps in a grid
align-content: flex-start;
display: flex !important;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
// Hide app icons when searching
.has-results ~ .o-app-menu-list {
display: none;
}
@include media-breakpoint-up(lg) {
padding: {
left: calc((100vw - 850px) / 2);
right: calc((100vw - 850px) / 2);
}
}
}

View File

@@ -2,95 +2,49 @@
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2021 ITerra - Sergey Shebanin
Copyright 2023 Onestein - Anjeel Haria
Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t t-inherit="web.NavBar.AppsMenu" t-inherit-mode="extension" owl="1">
<xpath expr="//Dropdown" position="replace">
<!-- Same hotkey as in EE -->
<t t-inherit="web.NavBar.AppsMenu" t-inherit-mode="extension">
<xpath expr="//Dropdown" position="attributes">
<attribute name="class">'o_navbar_apps_menu hide'</attribute>
<attribute name="skipTogglerTabbing">true</attribute>
<attribute name="hotkey" remove="'h'" add="'shift+h'" separator=" " />
</xpath>
<xpath expr="//Dropdown" position="after">
<AppsMenu>
<AppsMenuSearchBar />
<DropdownItem
<t t-set-slot="search_bar">
<AppsMenuSearchBar />
</t>
<AppMenuItem
t-foreach="apps"
t-as="app"
t-key="app.id"
class="'o_app'"
dataset="{ menuXmlid: app.xmlid, section: app.id }"
app="app"
currentApp="currentApp"
href="getMenuItemHref(app)"
onSelected="() => this.onNavBarDropdownItemSelection(app)"
>
<img
class="o-app-icon"
draggable="false"
t-att-src="getWebIconData(app)"
/>
<div t-esc="app.name" />
</DropdownItem>
onClick="onNavBarDropdownItemSelection.bind(this)"
/>
</AppsMenu>
</xpath>
</t>
<!-- Apps menu -->
<t t-name="web_responsive.AppsMenu" owl="1">
<div class="o-dropdown dropdown o-dropdown--no-caret o_navbar_apps_menu">
<button
class="dropdown-toggle"
title="Home Menu"
data-hotkey="a"
t-on-click.stop="() => this.setOpenState(!state.open,true)"
>
<i class="oi oi-apps" />
</button>
<div t-if="state.open" class="dropdown-menu-custom">
<t t-slot="default" />
</div>
</div>
</t>
<!-- Search bar -->
<t t-name="web_responsive.AppsMenuSearchResults" owl="1">
<div
class="search-container"
t-att-class="state.hasResults ? 'has-results' : ''"
>
<div class="search-input">
<span class="fa fa-search search-icon" />
<input
type="search"
t-ref="SearchBarInput"
t-on-input="_searchMenus"
t-on-keydown="_onKeyDown"
autocomplete="off"
placeholder="Search menus..."
class="form-control"
data-allow-hotkeys="true"
/>
</div>
<div t-if="state.results.length" class="search-results">
<t t-foreach="state.results" t-as="result" t-key="result">
<t t-set="menu" t-value="_menuInfo(result)" />
<a
t-attf-class="search-result {{result_index == state.offset ? 'highlight' : ''}}"
t-att-style="menu.webIconData ? &quot;background-image:url(&quot; + menu.webIconData + &quot;);background-size:4%&quot; : ''"
t-attf-href="#menu_id={{menu.id}}&amp;action={{menu.actionID}}"
t-att-data-menu-id="menu.id"
t-att-data-action-id="menu.actionID"
draggable="false"
>
<span class="text-ellipsis" t-att-title="result.name">
<t
t-foreach="_splitName(result)"
t-as="name"
t-key="name_index"
>
<b
t-if="name_index % 2"
t-out="name"
style="text-primary"
/>
<t t-else="" t-out="name" />
</t>
</span>
</a>
</t>
<!-- Apps menu -->
<t t-name="web_responsive.AppsMenu">
<div class="o_grid_apps_menu" t-att-data-theme="theme">
<button
class="o_grid_apps_menu__button"
title="Home Menu"
data-hotkey="h"
t-on-click.stop="onMenuClick"
>
<i class="oi oi-apps fs-4" />
</button>
<div t-if="state.open" class="app-menu-container">
<t t-slot="search_bar" />
<div class="o-app-menu-list">
<t t-slot="default" />
</div>
</div>
</div>
</t>

View File

@@ -0,0 +1,39 @@
/** @odoo-module **/
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {Component, xml} from "@odoo/owl";
import {registry} from "@web/core/registry";
import {useService} from "@web/core/utils/hooks";
class AppsMenuPreferences extends Component {
setup() {
this.action = useService("action");
this.user = useService("user");
}
async _onClick() {
const onClose = () => this.action.doAction("reload_context");
const action = await this.action.loadAction(
"web_responsive.res_users_view_form_apps_menu_preferences_action"
);
this.action.doAction({...action, res_id: this.user.userId}, {onClose}).then();
}
}
AppsMenuPreferences.template = xml`
<div class="o-dropdown dropdown o-dropdown--no-caret">
<button
role="button"
type="button"
title="App Menu Preferences"
class="dropdown-toggle o-dropdown--narrow"
t-on-click="_onClick">
<i class="fa fa-tint fa-lg px-1"/>
</button>
</div>
`;
registry
.category("systray")
.add("AppMenuTheme", {Component: AppsMenuPreferences}, {sequence: 100});

View File

@@ -0,0 +1,53 @@
/** @odoo-module **/
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {Component, onWillUpdateProps} from "@odoo/owl";
import {getWebIconData} from "@web_responsive/components/apps_menu_tools.esm";
export class AppMenuItem extends Component {
setup() {
super.setup();
this.webIconData = getWebIconData(this.props.app);
onWillUpdateProps(this.onUpdateProps);
}
get isActive() {
const {currentApp} = this.props;
return currentApp && currentApp.id === this.props.app.id;
}
get className() {
const classItems = ["o-app-menu-item"];
if (this.isActive) {
classItems.push("active");
}
return classItems.join(" ");
}
onUpdateProps(nextProps) {
this.webIconData = getWebIconData(nextProps.app);
}
onClick() {
if (typeof this.props.onClick === "function") {
this.props.onClick(this.props.app);
}
}
}
Object.assign(AppMenuItem, {
template: "web_responsive.AppMenuItem",
props: {
app: Object,
href: String,
currentApp: {
type: Object,
optional: true,
},
onClick: Function,
},
});

View File

@@ -0,0 +1,73 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
:root {
.o_grid_apps_menu[data-theme="milk"] {
--app-menu-text-color: #{$app-menu-text-color};
--app-menu-text-shadow: 1px 1px 1px #{rgba($white, 0.4)};
--app-menu-hover-background: #{rgba(white, 0.4)};
}
.o_grid_apps_menu[data-theme="community"] {
--app-menu-text-color: white;
--app-menu-text-shadow: 1px 1px 1px #{rgba(black, 0.4)};
--app-menu-hover-background: #{rgba(white, 0.2)};
}
}
.o-app-menu-item {
display: flex;
flex-direction: column;
border-radius: 4px;
gap: 0.25rem;
transition: ease box-shadow, transform, 0.3s;
background: unset;
outline: unset;
border: unset;
padding: 0.75rem 0.5rem;
justify-content: flex-start;
align-items: center;
white-space: normal;
user-select: none;
height: -moz-available;
height: max-content;
&__name {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 1em;
text-shadow: var(--app-menu-text-shadow);
color: var(--app-menu-text-color);
text-align: center;
}
&__icon {
height: auto;
max-width: 64px;
width: 64px;
aspect-ratio: 1;
padding: 10px;
background-color: white;
box-shadow: $app-menu-box-shadow;
}
&__active {
position: absolute;
bottom: 2px;
right: 2px;
text-shadow: 0 0 2px rgba(250, 250, 250, 0.6);
color: $app-menu-text-color;
}
&:focus,
&:hover {
transform: translateY(-4px);
box-shadow: 0 6px 12px -8px transparentize($app-menu-text-color, 0.6);
background-color: var(--app-menu-hover-background) !important;
backdrop-filter: blur(2px);
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2021 ITerra - Sergey Shebanin
Copyright 2023 Onestein - Anjeel Haria
Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t t-name="web_responsive.AppMenuItem">
<a
t-att-class="className"
role="button"
t-att-data-menu-xmlid="props.app.xmlid"
t-att-href="props.href"
t-on-click="onClick"
draggable="false"
>
<div
class="position-relative o_app"
t-att-data-menu-xmlid="props.app.xmlid"
>
<img
class="o-app-menu-item__icon rounded-3"
draggable="false"
t-att-src="webIconData"
/>
<i t-if="isActive" class="fa fa-check-circle o-app-menu-item__active" />
</div>
<span class="o-app-menu-item__name" t-att-title="props.app.name">
<t t-out="props.app.name" />
</span>
</a>
</t>
</templates>

View File

@@ -0,0 +1,77 @@
/** @odoo-module **/
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
export function getWebIconData(menu) {
const result = "/web_responsive/static/img/default_icon_app.png";
const iconData = menu.webIconData;
if (!iconData) {
return result;
}
const prefix = iconData.startsWith("P")
? "data:image/svg+xml;base64,"
: "data:image/png;base64,";
if (iconData.startsWith("data:image")) {
return iconData;
}
return prefix + iconData.replace(/\s/g, "");
}
/**
* @param {Object} menu
*/
export function updateMenuWebIconData(menu) {
menu.webIconData = menu.webIconData ? getWebIconData(menu) : "";
}
export function updateMenuDisplayName(menu) {
menu.displayName = menu.name.trim();
}
/**
* @param {Object} menu
* @returns {Boolean}
*/
export function isRootMenu(menu) {
return menu.actionID && menu.appID === menu.id;
}
/**
* @param {Object[]} memo
* @param {Object|null} parentMenu
* @param {Object} menu
* @returns {Object[]}
*/
export function collectSubMenuItems(memo, parentMenu, menu) {
const menuCopy = Object.assign({}, menu);
updateMenuDisplayName(menuCopy);
if (parentMenu) {
menuCopy.displayName = `${parentMenu.displayName} / ${menuCopy.displayName}`;
}
if (menuCopy.actionID && !isRootMenu(menuCopy)) {
memo.push(menuCopy);
}
for (const child of menuCopy.childrenTree || []) {
collectSubMenuItems(memo, menuCopy, child);
}
return memo;
}
/**
* @param {Object[]} memo
* @param {Object} menu
* @returns {Object}
*/
export function collectRootMenuItems(memo, menu) {
if (isRootMenu(menu)) {
const menuCopy = Object.assign({}, menu);
updateMenuWebIconData(menuCopy);
updateMenuDisplayName(menuCopy);
memo.push(menuCopy);
}
return memo;
}

View File

@@ -1,37 +0,0 @@
/** @odoo-module **/
/* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {AttachmentViewer} from "@mail/components/attachment_viewer/attachment_viewer";
import {patch} from "web.utils";
import {registerPatch} from "@mail/model/model_core";
const {useState} = owl;
// Patch attachment viewer to add min/max buttons capability
patch(AttachmentViewer.prototype, "web_responsive.AttachmentViewer", {
setup() {
this._super();
this.state = useState({
maximized: false,
});
},
});
registerPatch({
name: "Dialog",
fields: {
isCloseable: {
compute() {
if (this.attachmentViewer) {
/**
* Prevent closing the dialog when clicking on the mask when the user is
* currently dragging the image.
*/
return false;
}
return this._super();
},
},
},
});

View File

@@ -1,61 +0,0 @@
/* Copyright 2019 Tecnativa - Alexandre Díaz
* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
// Attachment Viewer
.o_web_client .o_DialogManager_dialog {
/* Show sided viewer on large screens */
@media (min-width: 1533px) {
&:not(:has(.o_AttachmentDeleteConfirm)) {
position: static;
}
.o_AttachmentViewer_main {
padding-bottom: 20px;
}
.o_AttachmentViewer {
// On-top of navbar
z-index: 10;
position: absolute;
right: 0;
top: 0;
bottom: 0;
margin-left: auto;
background-color: rgba(0, 0, 0, 0.7);
width: $chatter_zone_width;
&.o_AttachmentViewer_maximized {
width: 100% !important;
}
/* Show/Hide control buttons (next, prev, etc..) */
&:hover .o_AttachmentViewer_buttonNavigation,
&:hover .o_AttachmentViewer_toolbar {
display: flex;
}
.o_AttachmentViewer_buttonNavigation,
.o_AttachmentViewer_toolbar {
display: none;
}
.o_AttachmentViewer_viewIframe {
width: 95%;
}
}
}
@media (max-width: 1533px) {
.o_AttachmentViewer_headerItemButtonMinimize,
.o_AttachmentViewer_headerItemButtonMaximize {
display: none !important;
}
}
}
/* Attachment Viewer Max/Min buttons only are useful in sided mode */
.o_FormRenderer_chatterContainer:not(.o-aside) {
.o_AttachmentViewer_headerItemButtonMinimize,
.o_AttachmentViewer_headerItemButtonMaximize {
display: none !important;
}
}
.o_apps_menu_opened .o_AttachmentViewer {
display: none !important;
}

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2019 Tecnativa - Alexandre Díaz
Copyright 2021 Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t t-inherit="mail.AttachmentViewer" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_AttachmentViewer')]" position="attributes">
<attribute
name="t-att-class"
t-translation="off"
>state.maximized ? 'o_AttachmentViewer_maximized' : ''</attribute>
</xpath>
<xpath
expr="//div[hasclass('o_AttachmentViewer_headerItemButtonClose')]"
position="before"
>
<div
t-if="!state.maximized"
class="o_AttachmentViewer_headerItem o_AttachmentViewer_headerItemButton o_AttachmentViewer_headerItemButtonMaximize d-flex align-items-center mb-0 px-3 h4 text-reset cursor-pointer"
t-on-click="() => { state.maximized = true }"
role="button"
title="Maximize"
aria-label="Maximize"
>
<i class="fa fa-fw fa-window-maximize" role="img" />
</div>
<div
t-if="state.maximized"
class="o_AttachmentViewer_headerItem o_AttachmentViewer_headerItemButton o_AttachmentViewer_headerItemButtonMinimize d-flex align-items-center mb-0 px-3 h4 text-reset cursor-pointer"
t-on-click="() => { state.maximized = false }"
role="button"
title="Minimize"
aria-label="Minimize"
>
<i class="fa fa-fw fa-window-minimize" role="img" />
</div>
</xpath>
</t>
</template>

View File

@@ -0,0 +1,28 @@
/** @odoo-module **/
/* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {Chatter} from "@mail/core/web/chatter";
import {patch} from "@web/core/utils/patch";
import {useEffect} from "@odoo/owl";
patch(Chatter.prototype, {
setup() {
super.setup();
useEffect(this._resetScrollToAttachmentsEffect.bind(this), () => [
this.state.isAttachmentBoxOpened,
]);
},
/**
* Prevent scrollIntoView error
* @param {Boolean} isAttachmentBoxOpened
* @private
*/
_resetScrollToAttachmentsEffect(isAttachmentBoxOpened) {
if (!isAttachmentBoxOpened) {
this.state.scrollToAttachments = 0;
}
},
});

View File

@@ -0,0 +1,42 @@
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
.o-mail-Composer {
grid-template-areas:
"sidebar-header core-header"
"core-main core-main"
"sidebar-footer core-footer";
.o-mail-Composer-sidebarMain {
display: none;
}
@include media-breakpoint-up(sm) {
grid-template-areas:
"sidebar-header core-header"
"sidebar-main core-main"
"sidebar-footer core-footer";
.o-mail-Composer-sidebarMain {
display: block;
}
.o-mail-SuggestedRecipient {
margin-left: 42px;
}
}
}
.o-mail-Form-chatter {
.o-mail-SuggestedRecipient,
.o-mail-Chatter-recipientList {
margin-left: 0;
}
@include media-breakpoint-up(sm) {
.o-mail-SuggestedRecipient,
.o-mail-Chatter-recipientList {
margin-left: 42px;
}
}
}

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t
t-name="web_responsive.Chatter"
t-inherit="mail.Chatter"
t-inherit-mode="extension"
>
<xpath expr="//SuggestedRecipientsList" position="attributes">
<attribute name="styleString">''</attribute>
</xpath>
<xpath
expr="//button[hasclass('o-mail-Chatter-recipientListButton')]/.."
position="attributes"
>
<attribute name="style" />
<attribute name="class" add="o-mail-Chatter-recipientList" separator=" " />
</xpath>
<xpath
expr="//button[hasclass('o-mail-Chatter-sendMessage')]"
position="replace"
>
<button
t-if="props.hasMessageList"
class="o-mail-Chatter-sendMessage btn text-nowrap me-1"
t-att-class="{
'btn-secondary': state.composerType !== 'message',
'btn-primary active': state.composerType === 'message',
'my-2': !props.compactHeight
}"
t-att-disabled="!state.thread.hasWriteAccess and !(state.thread.hasReadAccess and state.thread.canPostOnReadonly) and props.threadId"
data-hotkey="m"
t-on-click="() => this.toggleComposer('message')"
>
<i class="fa fa-envelope me-sm-1" />
<span class="d-none d-sm-inline">Send message</span>
</button>
</xpath>
<xpath expr="//button[hasclass('o-mail-Chatter-logNote')]" position="replace">
<button
t-if="props.hasMessageList"
class="o-mail-Chatter-logNote btn text-nowrap me-2"
t-att-class="{
'btn-primary active': state.composerType === 'note',
'btn-secondary': state.composerType !== 'note',
'my-2': !props.compactHeight
}"
data-hotkey="shift+m"
t-on-click="() => this.toggleComposer('note')"
>
<i class="fa fa-sticky-note me-sm-1" />
<span class="d-none d-sm-inline">Log note</span>
</button>
</xpath>
<xpath
expr="//button[hasclass('o-mail-Chatter-activity')]/span"
position="before"
>
<i class="fa fa-clock-o me-sm-1" />
</xpath>
<xpath
expr="//button[hasclass('o-mail-Chatter-activity')]/span"
position="attributes"
>
<attribute name="class" add="d-none d-sm-inline" separator=" " />
</xpath>
</t>
</templates>

View File

@@ -1,15 +0,0 @@
/** @odoo-module **/
/* Copyright 2023 Onestein - Anjeel Haria
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {ChatterTopbar} from "@mail/components/chatter_topbar/chatter_topbar";
import {deviceContext} from "@web_responsive/components/ui_context.esm";
import {patch} from "web.utils";
// Patch chatter topbar to add ui device context
patch(ChatterTopbar.prototype, "web_responsive.ChatterTopbar", {
setup() {
this._super();
this.ui = deviceContext;
},
});

View File

@@ -1,223 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2023 Onestein - Anjeel Haria
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates xml:space="preserve">
<!-- Modifying the ChatterTopBar for Mobile View -->
<t
t-name="web.Responsivemail.ChatterTopbar"
t-inherit="mail.ChatterTopbar"
owl="1"
t-inherit-mode="extension"
>
<xpath expr="//div[contains(@class, 'o_ChatterTopbar')]" position="replace">
<t t-if="ui.isSmall">
<div
class="o_ChatterTopbar_rightSection d-flex border-bottom"
style="max-height:45%"
>
<button
t-if="chatterTopbar.chatter.thread.allAttachments.length === 0"
class="o_ChatterTopbar_button o_ChatterTopbar_buttonAddAttachments btn btn-light btn-primary"
type="button"
t-att-disabled="!chatterTopbar.chatter.isTemporary and !chatterTopbar.chatter.hasWriteAccess"
t-on-click="chatterTopbar.chatter.onClickButtonAddAttachments"
style="width:41%"
>
<i
class="fa fa-paperclip fa-lg me-1"
role="img"
aria-label="Attachments"
/>
<t t-if="chatterTopbar.chatter.isShowingAttachmentsLoading">
<i
class="o_ChatterTopbar_buttonAttachmentsCountLoader fa fa-circle-o-notch fa-spin"
aria-label="Attachment counter loading..."
/>
</t>
</button>
<button
t-if="chatterTopbar.chatter.thread.allAttachments.length > 0"
class="o_ChatterTopbar_button o_ChatterTopbar_buttonToggleAttachments btn btn-light btn-primary"
type="button"
t-att-disabled="!chatterTopbar.chatter.isTemporary and !chatterTopbar.chatter.hasReadAccess"
t-att-aria-expanded="chatterTopbar.chatter.attachmentBoxView ? 'true' : 'false'"
t-on-click="chatterTopbar.chatter.onClickButtonToggleAttachments"
style="width:41%"
>
<i
class="fa fa-paperclip fa-lg me-1"
role="img"
aria-label="Attachments"
/>
<t t-if="!chatterTopbar.chatter.isShowingAttachmentsLoading">
<span
class="o_ChatterTopbar_buttonCount o_ChatterTopbar_buttonAttachmentsCount"
t-esc="chatterTopbar.attachmentButtonText"
/>
</t>
<t t-if="chatterTopbar.chatter.isShowingAttachmentsLoading">
<i
class="o_ChatterTopbar_buttonAttachmentsCountLoader fa fa-circle-o-notch fa-spin"
aria-label="Attachment counter loading..."
/>
</t>
</button>
<t
t-if="chatterTopbar.chatter.hasFollowers and chatterTopbar.chatter.thread"
>
<FollowerListMenu
className="'o_ChatterTopbar_followerListMenu w-26'"
record="chatterTopbar.chatter.followerListMenuView"
/>
<t t-if="chatterTopbar.chatter.followButtonView">
<FollowButton
className="'o_ChatterTopbar_followButton'"
record="chatterTopbar.chatter.followButtonView"
/>
</t>
</t>
</div>
</t>
<div
class="o_ChatterTopbar justify-content-between d-flex"
t-attf-class="{{ className }}"
t-ref="root"
>
<div
class="o_ChatterTopbar_actions flex-fill d-flex border-transparent"
>
<div
class="o_ChatterTopbar_controllers d-flex pe-2"
t-if="chatterTopbar.chatter.threadView"
>
<button
class="o_ChatterTopbar_button o_ChatterTopbar_buttonSendMessage btn text-nowrap me-2"
type="button"
t-att-class="{
'o-active btn-odoo': chatterTopbar.chatter.composerView and !chatterTopbar.chatter.composerView.composer.isLog,
'btn-odoo': !chatterTopbar.chatter.composerView,
'btn-light': chatterTopbar.chatter.composerView and chatterTopbar.chatter.composerView.composer.isLog,
}"
t-att-disabled="!chatterTopbar.chatter.isTemporary and !chatterTopbar.chatter.hasWriteAccess"
data-hotkey="m"
t-on-click="chatterTopbar.chatter.onClickSendMessage"
>
Send message
</button>
<button
class="o_ChatterTopbar_button o_ChatterTopbar_buttonLogNote btn text-nowrap"
type="button"
t-att-class="{
'o-active btn-odoo': chatterTopbar.chatter.composerView and chatterTopbar.chatter.composerView.composer.isLog,
'btn-light': chatterTopbar.chatter.composerView and !chatterTopbar.chatter.composerView.composer.isLog or !chatterTopbar.chatter.composerView,
}"
t-att-disabled="!chatterTopbar.chatter.isTemporary and !chatterTopbar.chatter.hasWriteAccess"
t-on-click="chatterTopbar.chatter.onClickLogNote"
data-hotkey="shift+m"
>
Log note
</button>
</div>
<div
class="o_ChatterTopbar_tools position-relative d-flex flex-grow-1 border-bottom"
t-att-class="{
'border-start ps-2': chatterTopbar.chatter.hasActivities,
}"
>
<t t-if="chatterTopbar.chatter.hasActivities">
<button
class="o_ChatterTopbar_button o_ChatterTopbar_buttonScheduleActivity btn btn-light text-nowrap"
type="button"
t-att-disabled="!chatterTopbar.chatter.isTemporary and !chatterTopbar.chatter.hasWriteAccess"
t-on-click="chatterTopbar.chatter.onClickScheduleActivity"
data-hotkey="shift+a"
>
<i class="fa fa-clock-o me-1" />
<span>Activities</span>
</button>
</t>
<div
class="flex-grow-1 border-start pe-2"
t-att-class="{
'ms-2': chatterTopbar.chatter.hasActivities,
}"
/>
<t t-if="!ui.isSmall">
<div
class="o_ChatterTopbar_rightSection flex-grow-1 flex-shrink-0 justify-content-end d-flex"
>
<button
t-if="chatterTopbar.chatter.thread.allAttachments.length === 0"
class="o_ChatterTopbar_button o_ChatterTopbar_buttonAddAttachments btn btn-light btn-primary"
type="button"
t-att-disabled="!chatterTopbar.chatter.isTemporary and !chatterTopbar.chatter.hasWriteAccess"
t-on-click="chatterTopbar.chatter.onClickButtonAddAttachments"
>
<i
class="fa fa-paperclip fa-lg me-1"
role="img"
aria-label="Attachments"
/>
<t
t-if="chatterTopbar.chatter.isShowingAttachmentsLoading"
>
<i
class="o_ChatterTopbar_buttonAttachmentsCountLoader fa fa-circle-o-notch fa-spin"
aria-label="Attachment counter loading..."
/>
</t>
</button>
<button
t-if="chatterTopbar.chatter.thread.allAttachments.length > 0"
class="o_ChatterTopbar_button o_ChatterTopbar_buttonToggleAttachments btn btn-light btn-primary"
type="button"
t-att-disabled="!chatterTopbar.chatter.isTemporary and !chatterTopbar.chatter.hasReadAccess"
t-att-aria-expanded="chatterTopbar.chatter.attachmentBoxView ? 'true' : 'false'"
t-on-click="chatterTopbar.chatter.onClickButtonToggleAttachments"
>
<i
class="fa fa-paperclip fa-lg me-1"
role="img"
aria-label="Attachments"
/>
<t
t-if="!chatterTopbar.chatter.isShowingAttachmentsLoading"
>
<span
class="o_ChatterTopbar_buttonCount o_ChatterTopbar_buttonAttachmentsCount"
t-esc="chatterTopbar.attachmentButtonText"
/>
</t>
<t
t-if="chatterTopbar.chatter.isShowingAttachmentsLoading"
>
<i
class="o_ChatterTopbar_buttonAttachmentsCountLoader fa fa-circle-o-notch fa-spin"
aria-label="Attachment counter loading..."
/>
</t>
</button>
<t
t-if="chatterTopbar.chatter.hasFollowers and chatterTopbar.chatter.thread"
>
<FollowerListMenu
className="'o_ChatterTopbar_followerListMenu'"
record="chatterTopbar.chatter.followerListMenuView"
/>
<t t-if="chatterTopbar.chatter.followButtonView">
<FollowButton
className="'o_ChatterTopbar_followButton'"
record="chatterTopbar.chatter.followButtonView"
/>
</t>
</t>
</div>
</t>
</div>
</div>
</div>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,21 @@
/** @odoo-module **/
import {useState} from "@odoo/owl";
import {useService} from "@web/core/utils/hooks";
import {CommandPalette} from "@web/core/commands/command_palette";
import {patch} from "@web/core/utils/patch";
export const unpatchCommandPalette = patch(CommandPalette.prototype, {
setup() {
super.setup();
this.ui = useState(useService("ui"));
},
get small() {
return this.ui.size < 2;
},
get contentClass() {
return `o_command_palette ${this.small ? "" : "mt-5"}`;
},
});

View File

@@ -0,0 +1,28 @@
.o_command_palette {
.o_command_palette_exit {
display: none;
}
@include media-breakpoint-down(sm) {
.o_command_palette_root {
display: flex;
max-height: 100vh;
max-height: 100dvh;
flex-direction: column;
height: 100%;
justify-content: space-between;
}
.o_command_palette_exit {
display: block;
}
.o_command_palette_search {
flex-shrink: 0;
}
.o_command_palette_listbox {
max-height: unset;
}
.o_command_palette_footer {
flex-shrink: 0;
}
}
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<templates>
<t
t-name="web_responsive.CommandPalette"
t-inherit="web.CommandPalette"
t-inherit-mode="extension"
>
<xpath expr="//Dialog" position="attributes">
<attribute name="contentClass">contentClass</attribute>
</xpath>
<xpath expr="//div[@t-ref='root']" position="attributes">
<attribute name="class">o_command_palette_root</attribute>
</xpath>
<xpath expr="//div[hasclass('o_command_palette_search')]" position="before">
<div class="o_command_palette_exit">
<button
type="button"
class="btn btn-secondary w-100"
t-on-click="props.close"
>Exit</button>
</div>
</xpath>
</t>
</templates>

View File

@@ -1,45 +1,73 @@
/** @odoo-module **/
/* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import LegacyControlPanel from "web.ControlPanel";
import {ControlPanel} from "@web/search/control_panel/control_panel";
import {deviceContext} from "@web_responsive/components/ui_context.esm";
import {patch} from "web.utils";
import {Dropdown} from "@web/core/dropdown/dropdown";
import {patch} from "@web/core/utils/patch";
import {browser} from "@web/core/browser/browser";
const {useState} = owl;
export const STICKY_CLASS = "o_mobile_sticky";
// In v15.0 there are two ControlPanel's. They are mostly the same and are used in legacy and new owl views.
// We extend them two mostly the same way.
/**
* @param {Number} delay
* @returns {{collect: function(Number, (function(Number, Number): void)): void}}
*/
export function minMaxCollector(delay = 100) {
const state = {
id: null,
items: [],
};
// Patch legacy control panel to add states for mobile quick search
patch(LegacyControlPanel.prototype, "web_responsive.LegacyControlPanelMobile", {
function min() {
return Math.min.apply(null, state.items);
}
function max() {
return Math.max.apply(null, state.items);
}
return {
collect(value, callback) {
clearTimeout(state.id);
state.items.push(value);
state.id = setTimeout(() => {
callback(min(), max());
state.items = [];
state.id = null;
}, delay);
},
};
}
export const unpatchControlPanel = patch(ControlPanel.prototype, {
scrollValueCollector: undefined,
/** @type {Number}*/
scrollHeaderGap: undefined,
setup() {
this._super();
this.state = useState({
mobileSearchMode: this.props.withBreadcrumbs ? "" : "quick",
});
this.ui = deviceContext;
super.setup();
this.scrollValueCollector = minMaxCollector(100);
this.scrollHeaderGap = 2;
},
setMobileSearchMode(ev) {
this.state.mobileSearchMode = ev.detail;
onScrollThrottled() {
if (this.isScrolling) {
return;
}
this.isScrolling = true;
browser.requestAnimationFrame(() => (this.isScrolling = false));
/** @type {HTMLElement}*/
const rootEl = this.root.el;
const scrollTop = this.getScrollingElement().scrollTop;
const activeAnimation = scrollTop > this.initialScrollTop;
rootEl.classList.toggle(STICKY_CLASS, activeAnimation);
this.scrollValueCollector.collect(scrollTop - this.oldScrollTop, (min, max) => {
const delta = min + max;
if (delta < -this.scrollHeaderGap || delta > this.scrollHeaderGap) {
rootEl.style.top = `${delta < 0 ? -rootEl.clientHeight : 0}px`;
}
});
this.oldScrollTop = scrollTop;
},
});
// Patch control panel to add states for mobile quick search
patch(ControlPanel.prototype, "web_responsive.ControlPanelMobile", {
setup() {
this._super();
this.state = useState({
mobileSearchMode: "",
});
this.ui = deviceContext;
},
setMobileSearchMode(ev) {
this.state.mobileSearchMode = ev.detail;
},
});
Object.assign(LegacyControlPanel.components, {Dropdown});

View File

@@ -1,306 +0,0 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
// Make enough space for search panel filters buttons
.o_control_panel {
// There is no media breakpoint for XL upper bound
@include media-breakpoint-up(lg) {
@media (max-width: 1360px) {
.o_cp_top_left,
.o_cp_bottom_left {
width: 40%;
}
.o_cp_top_right,
.o_cp_bottom_right {
width: 60%;
}
}
}
// For FULL HD devices
@media (min-width: 1900px) {
.o_cp_top_left,
.o_cp_bottom_left {
width: 60%;
}
.o_cp_top_right,
.o_cp_bottom_right {
width: 40%;
}
}
@include media-breakpoint-only(md) {
.o_search_options_hide_labels .o_dropdown_title {
display: none;
}
}
.o_cp_bottom_right {
height: 10%;
}
// Mobile Control panel (breadcrumbs, search box, buttons...)
@include media-breakpoint-down(sm) {
// Avoid horizontal scrolling of control panel.
// It doesn't work on iOS Safari, but it looks similar as
// without this patch. With this patch it looks better for
// other browsers.
// Arrange buttons to use space better
.o_cp_top_left,
.o_cp_top_right {
flex: 1 1 100%;
}
.o_cp_top_left {
flex-basis: 89%;
max-width: 89%;
}
.o_cp_top_right {
flex-basis: 11%;
}
.o_cp_bottom {
position: relative; // Necessary for dropdown menu positioning
display: block;
margin: 0;
min-height: 30px !important;
}
.o_cp_bottom_left {
float: left;
margin: 5px 0;
}
.o_cp_bottom_right {
float: right;
padding-left: 10px;
margin: 5px 0;
}
.o_cp_bottom_right,
.o_cp_pager {
white-space: nowrap;
}
.o_cp_pager {
margin-bottom: 0;
}
.o_list_selection_box {
padding-left: 5px !important;
padding-right: 5px;
}
.o_cp_action_menus {
padding-right: 0;
.o_dropdown_title,
.fa-chevron-right,
.fa-chevron-down {
display: none;
}
.dropdown-toggle {
margin: 0px 2px;
height: 100%;
}
.dropdown {
height: 100%;
}
@include media-breakpoint-down(xs) {
.dropdown {
position: static;
}
.dropdown-menu {
right: 0;
left: 0;
top: 35px;
}
}
}
// Hide all but 2 last breadcrumbs, and render 2nd-to-last as arrow
.breadcrumb-item {
&:not(.active):not(.o_back_button) {
padding-left: 0;
display: none;
}
&::before {
content: none;
padding-right: 0;
}
&.o_back_button {
&::before {
color: var(--primary);
content: "\f060"; // .fa-arrow-left
cursor: pointer;
font-family: FontAwesome;
}
a {
display: none;
}
}
}
// Ellipsize long breadcrumbs
.breadcrumb {
max-width: 100%;
text-overflow: ellipsis;
}
// In case you install `mail`, there is a mess on BS vs inline styles
// we need to fix
.o_cp_buttons .btn.d-block:not(.d-none) {
display: inline-block !important;
}
.o_searchview_input_container > .o_searchview_autocomplete {
left: 0;
right: 0;
> li {
padding: 10px 0px;
}
}
.o_searchview_quick {
display: flex;
flex: 1 1 auto;
align-items: center;
.o_searchview_input_container {
flex: 1 1 auto;
margin-left: 5px;
}
}
.o_searchview {
padding: 1px 0px 3px 0px;
&.o_searchview_mobile {
cursor: pointer;
}
}
}
// Filter Menu
// Cut long filters names in the filters menu
.o_filter_menu {
.o_menu_item {
@include media-breakpoint-up(md) {
max-width: 250px;
}
a {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
// Enable scroll on dropdowns
.o_cp_buttons .dropdown-menu {
max-height: 70vh;
overflow-y: auto;
overflow-x: hidden;
}
// Dropdown with buttons to switch the view type
.o_cp_switch_buttons.dropdown-menu {
align-content: center;
display: flex;
flex-direction: row;
justify-content: space-around;
padding: 0;
.btn {
border: {
bottom: 0;
radius: 0;
top: 0;
}
font-size: 1.3em;
}
}
}
// Mobile search bar full screen mode
.o_cp_mobile_search {
position: fixed;
top: 0;
left: 0;
bottom: 0;
padding: 0;
width: 100%;
background-color: white;
z-index: $zindex-modal;
overflow: auto;
.o_mobile_search_header {
background-color: var(--mobileSearch__header-bg, #{$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_searchview_input_container {
display: flex;
padding: 15px 20px 0 20px;
position: relative;
.o_searchview_input {
width: 100%;
margin-bottom: 15px;
border-bottom: 1px solid $o-brand-primary;
}
.o_searchview_facet {
display: inline-flex;
order: 1;
}
.o_searchview_autocomplete {
top: 3rem;
}
}
.o_mobile_search_filter {
padding-bottom: 15%;
> .dropdown {
flex-direction: column;
line-height: 2rem;
width: 100%;
margin: 15px 5px 0px 5px;
border: solid 1px darken($gray-200, 20%);
}
.dropdown.show > .dropdown-toggle {
background-color: $gray-200;
}
.dropdown-toggle {
width: 100%;
text-align: left;
&:after {
top: auto;
}
}
.dropdown-item:before {
top: auto;
}
.dropdown-item.focus {
background-color: white;
}
.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%;
max-height: 100%;
box-shadow: none;
border: none;
color: $gray-600;
.divider {
margin: 0px;
}
> li > a {
padding: 10px 26px;
}
}
}
.o_mobile_search_show_result {
padding: 10px;
font-size: 15px;
}
}

View File

@@ -1,227 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2021 Sergey Shebanin
Copyright 2023 Onestein - Anjeel Haria
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<!-- Legacy control panel templates -->
<t t-inherit="web.Legacy.ControlPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//nav[hasclass('o_cp_switch_buttons')]" position="replace">
<t t-if="props.views.length gt 1">
<t t-if="ui.size lt= ui.SIZES.LG">
<Dropdown
position="'bottom-end'"
menuClass="'d-inline-flex o_cp_switch_buttons'"
togglerClass="'btn btn-link'"
>
<t t-set-slot="toggler">
<i
class="fa fa-lg o_switch_view"
t-attf-class="o_{{env.view.type}} {{env.view.icon}} {{ props.views.filter(view => view.type === env.view.type)[0].icon }} {{env.view.active ? 'active' : ''}}"
/>
</t>
<t t-foreach="props.views" t-as="view" t-key="view.type">
<t t-call="web.ViewSwitcherButton" />
</t>
</Dropdown>
</t>
<t t-else="">
<nav
class="btn-group o_cp_switch_buttons"
role="toolbar"
aria-label="View switcher"
>
<t t-foreach="props.views" t-as="view" t-key="view.type">
<t t-call="web.ViewSwitcherButton" />
</t>
</nav>
</t>
</t>
</xpath>
<xpath expr="//div[hasclass('o_searchview')]" position="replace">
<div
t-if="props.withSearchBar"
class="o_searchview"
t-att-class="state.mobileSearchMode == 'quick' ? 'o_searchview_quick' : 'o_searchview_mobile'"
role="search"
aria-autocomplete="list"
t-on-click.self="() => { state.mobileSearchMode = ui.isSmall ? 'quick' : '' }"
>
<t t-if="!ui.isSmall">
<i
class="o_searchview_icon fa fa-search"
title="Search..."
role="img"
aria-label="Search..."
/>
<SearchBar fields="fields" />
</t>
<t t-if="ui.isSmall">
<t t-if="state.mobileSearchMode == 'quick'">
<button
t-if="props.withBreadcrumbs"
class="btn btn-link fa fa-arrow-left"
t-on-click.stop="() => { state.mobileSearchMode = '' }"
/>
<SearchBar fields="fields" />
<button
class="btn fa fa-filter"
t-on-click.stop="() => { state.mobileSearchMode = 'full' }"
/>
</t>
<t
t-if="state.mobileSearchMode == 'full'"
t-call="web_responsive.LegacyMobileSearchView"
/>
<t t-if="state.mobileSearchMode == ''">
<button
class="btn btn-link fa fa-search"
t-on-click.stop="() => { state.mobileSearchMode = 'quick' }"
/>
</t>
</t>
</div>
</xpath>
<xpath expr="//div[hasclass('o_cp_top_left')]" position="attributes">
<attribute
name="t-att-class"
t-translation="off"
>ui.isSmall and state.mobileSearchMode == 'quick' ? 'o_hidden' : ''</attribute>
</xpath>
<xpath expr="//div[hasclass('o_search_options')]" position="attributes">
<attribute name="t-if" t-translation="off">!ui.isSmall</attribute>
<attribute
name="t-att-class"
t-translation="off"
>ui.size == ui.SIZES.MD ? 'o_search_options_hide_labels' : ''</attribute>
</xpath>
</t>
<t t-name="web_responsive.LegacyMobileSearchView" owl="1">
<div class="o_cp_mobile_search">
<div class="o_mobile_search_header">
<button
type="button"
class="o_mobile_search_button btn"
t-on-click="() => state.mobileSearchMode = 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="() => this.model.dispatch('clearQuery')"
>
CLEAR
</button>
</div>
<SearchBar fields="fields" />
<div class="o_mobile_search_filter o_search_options mb8 mt8 ml16 mr16">
<FilterMenu
t-if="props.searchMenuTypes.includes('filter')"
class="o_filter_menu"
fields="fields"
/>
<GroupByMenu
t-if="props.searchMenuTypes.includes('groupBy')"
class="o_group_by_menu"
fields="fields"
/>
<ComparisonMenu
t-if="props.searchMenuTypes.includes('comparison') and model.get('filters', f => f.type === 'comparison').length"
class="o_comparison_menu"
/>
<FavoriteMenu
t-if="props.searchMenuTypes.includes('favorite')"
class="o_favorite_menu"
/>
</div>
<div
class="btn btn-primary o_mobile_search_show_result fixed-bottom"
t-on-click="() => { state.mobileSearchMode = (props.withBreadcrumbs ? '' : 'quick') }"
>
<t>SEE RESULT</t>
</div>
</div>
</t>
<t t-name="web_responsive.SearchBar" owl="1">
<div>
<t t-if="!env.isSmall" t-call="web.SearchBar" />
<t t-if="env.isSmall">
<t t-if="props.mobileSearchMode == 'quick'">
<div class="o_searchview o_searchview_quick">
<button
t-if="props.withBreadcrumbs"
class="btn btn-link fa fa-arrow-left"
t-on-click.stop="() => this.trigger('set-mobile-view', '')"
/>
<div class="o_searchview_input_container">
<t t-call="web.SearchBar.Facets" />
<t t-call="web.SearchBar.Input" />
<t t-if="items.length">
<t t-call="web.SearchBar.Items" />
</t>
</div>
<button
class="btn fa fa-filter"
t-on-click.stop="() => this.trigger('set-mobile-view', 'full')"
/>
</div>
</t>
<t
t-if="props.mobileSearchMode == 'full'"
t-call="web_responsive.MobileSearchView"
/>
<t t-if="props.mobileSearchMode == ''">
<div
class="o_searchview o_searchview_mobile"
role="search"
aria-autocomplete="list"
t-on-click.stop="() => this.trigger('set-mobile-view', 'quick')"
>
<button class="btn btn-link fa fa-search" />
</div>
</t>
</t>
</div>
</t>
<t t-name="web_responsive.MobileSearchView" owl="1">
<div class="o_searchview">
<div class="o_cp_mobile_search">
<div class="o_mobile_search_header">
<span
class="o_mobile_search_close float-left mt16 mb16 mr8 ml16"
t-on-click.stop="() => this.trigger('set-mobile-view', 'quick')"
>
<i class="fa fa-arrow-left" />
<strong class="float-right ml8">FILTER</strong>
</span>
<span
class="float-right o_mobile_search_clear_facets mt16 mr16"
t-on-click.stop="() => env.searchModel.clearQuery()"
>
<t>CLEAR</t>
</span>
</div>
<div class="o_searchview_input_container">
<t t-call="web.SearchBar.Facets" />
<t t-call="web.SearchBar.Input" />
<t t-if="items.length">
<t t-call="web.SearchBar.Items" />
</t>
</div>
<div class="o_mobile_search_filter o_search_options mb8 mt8 ml16 mr16">
<t t-foreach="props.searchMenus" t-as="menu" t-key="menu.key">
<t t-component="menu.Component" />
</t>
</div>
<div
class="btn btn-primary o_mobile_search_show_result fixed-bottom"
t-on-click.stop="() => this.trigger('set-mobile-view', '')"
>
<t>SEE RESULT</t>
</div>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,77 @@
/** @odoo-module **/
/* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {onMounted, onWillStart, useExternalListener, useRef} from "@odoo/owl";
import {FileViewer} from "@web/core/file_viewer/file_viewer";
import {patch} from "@web/core/utils/patch";
const formChatterClassName = ".o-mail-Form-chatter";
const formViewSheetClassName = ".o_form_view_container .o_form_sheet_bg";
export function useFileViewerContainerSize(ref) {
function updateActualFormChatterSize() {
/** @type {HTMLDivElement}*/
const chatterElement = document.querySelector(formChatterClassName);
/** @type {HTMLDivElement}*/
const formSheetElement = document.querySelector(formViewSheetClassName);
if (chatterElement && formSheetElement && ref.el) {
/** @type {CSSStyleDeclaration}*/
const elStyle = ref.el.style;
const width = `${chatterElement.clientWidth}px`;
const height = `${chatterElement.clientHeight}px`;
const left = `${formSheetElement.clientWidth}px`;
elStyle.setProperty("--o-FileViewerContainer-width", width);
elStyle.setProperty("--o-FileViewerContainer-height", height);
elStyle.setProperty("--o-FileViewerContainer-left", left);
}
}
useExternalListener(window, "resize", () => {
requestAnimationFrame(updateActualFormChatterSize);
});
onMounted(() => {
requestAnimationFrame(updateActualFormChatterSize);
});
}
/**
* Patch attachment viewer to add min/max buttons capability
* @property {Function} resizeUpdateActualFormChatterWidth
*/
patch(FileViewer.prototype, {
setup() {
super.setup();
this.root = useRef("root");
Object.assign(this.state, {
allowMinimize: false,
maximized: true,
});
useFileViewerContainerSize(this.root);
onWillStart(this.setDefaultMaximizeState);
},
get rootClass() {
return {
modal: this.props.modal,
"o-FileViewerContainer__maximized": this.state.maximized,
"o-FileViewerContainer__minimized": !this.state.maximized,
};
},
setDefaultMaximizeState() {
this.state.allowMinimize = Boolean(
document.querySelector(`${formChatterClassName}.o-aside`)
);
this.state.maximized = !this.state.allowMinimize;
},
/**
* @param {Boolean} value
*/
setMaximized(value) {
this.state.maximized = value;
},
});

View File

@@ -0,0 +1,56 @@
/* Copyright 2019 Tecnativa - Alexandre Díaz
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
.o-FileViewerContainer {
--o-FileViewerContainer-width: #{$o-mail-Chatter-minWidth};
--o-FileViewerContainer-height: var(--100vh, calc(100vh - #{$o-navbar-height}));
--o-FileViewerContainer-left: unset;
--o-FileViewerContainer-right: 0;
position: fixed;
right: 0;
z-index: $zindex-fixed;
&__maximized {
top: 0;
left: 0;
right: 0;
}
&__minimized {
width: 100%;
max-width: var(--o-FileViewerContainer-width, #{$o-mail-Chatter-minWidth});
height: var(--o-FileViewerContainer-height);
top: unset;
right: var(--o-FileViewerContainer-right, 0);
left: var(--o-FileViewerContainer-left, unset);
bottom: 0;
.o-FileViewer-main {
padding: $o-navbar-height 0 0 0;
}
.o-FileViewer-viewPdf {
width: 100% !important;
}
}
.o-FileViewer-navigation {
background-color: rgba(255, 255, 255, 0.2);
text-shadow: 0 0 rgba(30, 30, 30, 0.8);
box-shadow: 0 0 1px 0 rgba(30, 30, 30, 0.4);
transition: background-color 0.2s, box-shadow 0.2s;
&:hover {
background-color: rgba(255, 255, 255, 0.8);
text-shadow: 0 0 black;
box-shadow: 0 0 2px 0 rgba(30, 30, 30, 0.8);
}
}
}
.o_apps_menu_opened .o-FileViewerContainer {
display: none !important;
}

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2019 Tecnativa - Alexandre Díaz
Copyright 2021 Sergey Shebanin
Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t
t-name="web_responsive.FileViewer"
t-inherit="web.FileViewer"
t-inherit-mode="extension"
>
<xpath expr="div[hasclass('justify-content-center')]" position="attributes">
<attribute name="class" add="o-FileViewerContainer" separator=" " />
<attribute name="t-att-class">rootClass</attribute>
<attribute name="t-ref">root</attribute>
</xpath>
<xpath expr="//iframe[@t-ref='iframeViewerPdf']" position="attributes">
<attribute name="class" add="o-FileViewer-viewPdf" separator=" " />
</xpath>
<xpath expr="//div[@t-on-click.stop='close']" position="before">
<t t-if="state.allowMinimize">
<div
t-if="!state.maximized"
t-on-click="setMaximized.bind(this, true)"
class="o-FileViewer-headerButton d-flex align-items-center mb-0 px-3 h4 text-reset cursor-pointer"
role="button"
name="maximize"
title="Maximize"
aria-label="Maximize"
>
<i class="fa fa-fw fa-window-maximize" role="img" />
</div>
<div
t-if="state.maximized"
class="o-FileViewer-headerButton d-flex align-items-center mb-0 px-3 h4 text-reset cursor-pointer"
t-on-click="setMaximized.bind(this, false)"
role="button"
name="minimize"
title="Minimize"
aria-label="Minimize"
>
<i class="fa fa-fw fa-window-minimize" role="img" />
</div>
</t>
</xpath>
</t>
</template>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2021 ITerra - Sergey Shebanin
Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates id="template" xml:space="preserve">
@@ -11,7 +12,6 @@
>
<attribute
name="t-value"
t-translation="off"
>'shift+' + ((section_index + 1) % 10).toString()</attribute>
</xpath>
<xpath
@@ -20,7 +20,6 @@
>
<attribute
name="t-value"
t-translation="off"
>'shift+' + (sectionsVisibleCount + 1 % 10).toString()</attribute>
</xpath>
</t>

View File

@@ -0,0 +1,207 @@
/** @odoo-module **/
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {Component, onPatched, onWillPatch, useRef, useState} from "@odoo/owl";
import {
collectRootMenuItems,
collectSubMenuItems,
} from "@web_responsive/components/apps_menu_tools.esm";
import {useAutofocus, useService} from "@web/core/utils/hooks";
import {debounce} from "@web/core/utils/timing";
import {escapeRegExp} from "@web/core/utils/strings";
import {fuzzyLookup} from "@web/core/utils/search";
import {scrollTo} from "@web/core/utils/scrolling";
/**
* @extends Component
*/
export class AppsMenuCanonicalSearchBar extends Component {
setup() {
super.setup();
this.state = useState({
rootItems: [],
subItems: [],
offset: 0,
hasResults: false,
});
this.searchBarInput = useAutofocus({refName: "SearchBarInput"});
this._searchMenus = debounce(this._searchMenus, 200);
this.menuService = useService("menu");
this.searchItemsRef = useRef("searchItems");
this.rootMenuItems = this.getRootMenuItems();
this.subMenuItems = this.getSubMenuItems();
onWillPatch(this._computeResultOffset);
onPatched(this._scrollToHighlight);
}
/**
* @returns {String}
*/
get inputValue() {
const {el} = this.searchBarInput;
return el ? el.value : "";
}
/**
* @returns {Boolean}
*/
get hasItemsToDisplay() {
return this.totalItemsCount > 0;
}
/**
* @returns {Number}
*/
get totalItemsCount() {
const {rootItems, subItems} = this.state;
return rootItems.length + subItems.length;
}
/**
* @param {Number} index
* @param {Boolean} isSubMenu
* @returns {String}
*/
highlighted(index, isSubMenu = false) {
const {state} = this;
let _index = index;
if (isSubMenu) {
_index = state.rootItems.length + index;
}
return _index === state.offset ? "highlight" : "";
}
/**
* @returns {Object[]}
*/
getRootMenuItems() {
return this.menuService.getApps().reduce(collectRootMenuItems, []);
}
/**
* @returns {Object[]}
*/
getSubMenuItems() {
const response = [];
for (const menu of this.menuService.getApps()) {
const menuTree = this.menuService.getMenuAsTree(menu.id);
collectSubMenuItems(response, null, menuTree);
}
return response;
}
/**
* Search among available menu items, and render that search.
*/
_searchMenus() {
const {state} = this;
const query = this.inputValue;
state.hasResults = query !== "";
if (!state.hasResults) {
state.rootItems = [];
state.subItems = [];
return;
}
const searchField = (item) => item.displayName;
state.rootItems = fuzzyLookup(query, this.rootMenuItems, searchField);
state.subItems = fuzzyLookup(query, this.subMenuItems, searchField);
}
_onKeyDown(ev) {
const code = ev.code;
if (code === "Escape") {
ev.stopPropagation();
ev.preventDefault();
if (this.inputValue) {
this.searchBarInput.el.value = "";
Object.assign(this.state, {rootItems: [], subItems: []});
this.state.hasResults = false;
} else {
this.env.bus.trigger("ACTION_MANAGER:UI-UPDATED");
}
} else if (code === "Tab") {
if (this.searchItemsRef.el) {
ev.preventDefault();
if (ev.shiftKey) {
this.state.offset--;
} else {
this.state.offset++;
}
}
} else if (code === "ArrowUp") {
if (this.searchItemsRef.el) {
ev.preventDefault();
this.state.offset--;
}
} else if (code === "ArrowDown") {
if (this.searchItemsRef.el) {
ev.preventDefault();
this.state.offset++;
}
} else if (code === "Enter") {
const element = this.searchItemsRef.el;
if (this.hasItemsToDisplay && element) {
ev.preventDefault();
this._selectHighlightedSearchItem(element);
}
} else if (code === "Home") {
this.state.offset = 0;
} else if (code === "End") {
this.state.offset = this.totalItemsCount - 1;
}
}
/**
* @param {HTMLElement} element
* @private
*/
_selectHighlightedSearchItem(element) {
const highlightedElement = element.querySelector(
".highlight > .search-item__link"
);
if (highlightedElement) {
highlightedElement.click();
} else {
console.warn("Highlighted search item is not found");
}
}
_splitName(name) {
if (!name) {
return [];
}
const value = this.inputValue;
const splitName = name.split(new RegExp(`(${escapeRegExp(value)})`, "ig"));
return value.length && splitName.length > 1 ? splitName : [name];
}
_scrollToHighlight() {
// Scroll to selected element on keyboard navigation
const element = this.searchItemsRef.el;
if (!(this.totalItemsCount && element)) {
return;
}
const activeElement = element.querySelector(".highlight");
if (activeElement) {
scrollTo(activeElement, element);
}
}
_computeResultOffset() {
// Allow looping on results
const {state} = this;
const total = this.totalItemsCount;
if (state.offset < 0) {
state.offset = total + state.offset;
} else if (state.offset >= total) {
state.offset -= total;
}
}
}
AppsMenuCanonicalSearchBar.props = {};
AppsMenuCanonicalSearchBar.template = "web_responsive.AppsMenuCanonicalSearchBar";

View File

@@ -0,0 +1,112 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
:root {
.o_grid_apps_menu[data-theme="milk"] {
--apps-menu-scrollbar-background: #{$o-brand-odoo};
--apps-menu-empty-search-color: $app-menu-text-color;
}
.o_grid_apps_menu[data-theme="community"] {
--apps-menu-scrollbar-background: white;
--apps-menu-empty-search-color: white;
}
}
.o_grid_apps_menu .search-container {
// Allow to scroll only on results, keeping static search box above
.search-list {
display: flex;
flex-direction: column;
gap: calc(0.25rem + 1px);
overflow: auto;
padding: 0.25rem 0;
margin: 0.25rem 0;
max-height: calc(100vh - #{$o-navbar-height} - 5.25rem);
max-height: calc(100dvh - #{$o-navbar-height} - 5.25rem);
max-width: calc(100vw - 1rem);
position: relative;
width: 100%;
height: 100%;
&::-webkit-scrollbar {
width: 10px;
}
&::-webkit-scrollbar-thumb {
background: var(--apps-menu-scrollbar-background);
border-radius: 6px;
}
@include media-breakpoint-down(md) {
&::-webkit-scrollbar {
width: 4px;
}
}
}
.search-item-divider {
margin: 0 4px;
hr {
margin: 0.5rem 0;
background-color: $o-brand-odoo;
}
}
.search-item {
display: block;
align-items: center;
background-position: left;
background-repeat: no-repeat;
background-size: contain;
white-space: normal;
font-weight: 100;
background-color: white;
box-shadow: $app-menu-box-shadow;
margin: 0 4px;
border-radius: 4px;
&__link {
display: flex;
gap: 0.5rem;
padding: 0.25rem 0.5rem;
align-items: center;
cursor: pointer;
}
&__name {
color: $app-menu-text-color;
text-shadow: 0 0 $app-menu-text-color;
}
&__image {
max-height: 40px;
max-width: 40px;
width: 40px;
object-fit: contain;
padding: 4px;
}
&.highlight,
&:hover {
background-color: $app-menu-item-highlight;
box-shadow: $app-menu-box-shadow-highlight;
font-weight: 300;
}
b {
font-weight: 700;
}
}
.empty-search-item {
display: inline-block;
width: 100%;
text-align: center;
padding: 0.25rem 0.5rem;
color: var(--apps-menu-empty-search-color);
}
}

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2021 ITerra - Sergey Shebanin
Copyright 2023 Onestein - Anjeel Haria
Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<!-- Search bar -->
<t t-name="web_responsive.AppsMenuCanonicalSearchBar">
<div class="search-container" t-att-class="{'has-results': state.hasResults}">
<div class="search-input">
<i class="fa fa-search search-icon fs-4 my-auto d-none d-sm-flex" />
<input
type="search"
t-ref="SearchBarInput"
t-on-input="_searchMenus"
t-on-keydown="_onKeyDown"
autocomplete="off"
placeholder="Search menus..."
class="form-control"
/>
</div>
<ul
t-if="hasItemsToDisplay"
class="list-unstyled search-list"
t-ref="searchItems"
>
<t t-foreach="state.rootItems" t-as="menu" t-key="menu.xmlid">
<li t-attf-class="search-item {{highlighted(menu_index)}}">
<a
t-attf-class="search-item__link"
t-attf-href="#menu_id={{menu.id}}&amp;action={{menu.actionID}}"
t-att-data-menu-id="menu.id"
t-att-data-action-id="menu.actionID"
draggable="false"
tabindex="-1"
>
<img
class="search-item__image"
t-att-src="menu.webIconData"
alt="App Icon"
/>
<span class="search-item__name" t-att-title="menu.name">
<t
t-foreach="_splitName(menu.displayName)"
t-as="name"
t-key="name_index"
>
<b t-if="name_index % 2" t-out="name" />
<t t-else="" t-out="name" />
</t>
</span>
</a>
</li>
</t>
<li
class="search-item-divider"
t-if="state.rootItems.length and state.subItems.length"
>
<hr class="w-100" />
</li>
<t t-foreach="state.subItems" t-as="menu" t-key="menu.xmlid">
<li t-attf-class="search-item {{highlighted(menu_index, true)}}">
<a
t-attf-class="search-item__link"
t-attf-href="#menu_id={{menu.id}}&amp;action={{menu.actionID}}"
t-att-data-menu-id="menu.id"
t-att-data-action-id="menu.actionID"
draggable="false"
tabindex="-1"
>
<span
class="search-item__name px-2 py-1"
t-att-title="menu.name"
>
<t
t-foreach="_splitName(menu.displayName)"
t-as="name"
t-key="name_index"
>
<b t-if="name_index % 2" t-out="name" />
<t t-else="" t-out="name" />
</t>
</span>
</a>
</li>
</t>
</ul>
<ul
t-if="!hasItemsToDisplay and inputValue"
class="list-unstyled search-list"
>
<li class="empty-search-item">
<strong>Nothing to show</strong>
</li>
</ul>
</div>
</t>
</templates>

View File

@@ -0,0 +1,32 @@
/** @odoo-module **/
/* global Fuse */
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {AppsMenuCanonicalSearchBar} from "@web_responsive/components/menu_canonical_searchbar/searchbar.esm";
/**
* @extends AppsMenuCanonicalSearchBar
*/
export class AppsMenuFuseSearchBar extends AppsMenuCanonicalSearchBar {
setup() {
super.setup();
this.fuseOptions = {
keys: ["displayName"],
threshold: 0.43,
};
this.rootMenuItems = new Fuse(this.getRootMenuItems(), this.fuseOptions);
this.subMenuItems = new Fuse(this.getSubMenuItems(), this.fuseOptions);
}
_searchMenus() {
const {state} = this;
const query = this.inputValue;
state.hasResults = query !== "";
state.rootItems = this.rootMenuItems.search(query);
state.subItems = this.subMenuItems.search(query);
}
}
AppsMenuFuseSearchBar.props = {};
AppsMenuFuseSearchBar.template = "web_responsive.AppsMenuFuseSearchBar";

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<!-- Search bar -->
<t
t-name="web_responsive.AppsMenuFuseSearchBar"
t-inherit="web_responsive.AppsMenuCanonicalSearchBar"
t-inherit-mode="primary"
>
<xpath expr="//t[@t-foreach='state.rootItems']" position="attributes">
<attribute name="t-as">result</attribute>
<attribute name="t-key">result.item.xmlid</attribute>
</xpath>
<xpath expr="//t[@t-foreach='state.rootItems']/li" position="before">
<t t-set="menu" t-value="result.item" />
</xpath>
<xpath expr="//t[@t-foreach='state.rootItems']/li" position="attributes">
<attribute
name="t-attf-class"
>search-item {{highlighted(result_index)}}</attribute>
</xpath>
<xpath expr="//t[@t-foreach='state.subItems']" position="attributes">
<attribute name="t-as">result</attribute>
<attribute name="t-key">result.item.xmlid</attribute>
</xpath>
<xpath expr="//t[@t-foreach='state.subItems']/li" position="before">
<t t-set="menu" t-value="result.item" />
</xpath>
<xpath expr="//t[@t-foreach='state.subItems']/li" position="attributes">
<attribute
name="t-attf-class"
>search-item {{highlighted(result_index, true)}}</attribute>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,65 @@
/** @odoo-module **/
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {Component, useState} from "@odoo/owl";
import {useAutofocus, useService} from "@web/core/utils/hooks";
/**
* @extends Component
* @property {{el: HTMLInputElement}} searchBarInput
*/
export class AppsMenuOdooSearchBar extends Component {
setup() {
super.setup();
this.state = useState({
rootItems: [],
subItems: [],
offset: 0,
hasResults: false,
});
this.searchBarInput = useAutofocus({refName: "SearchBarInput"});
this.command = useService("command");
}
/**
* @returns {String}
*/
get inputValue() {
const {el} = this.searchBarInput;
return el ? el.value : "";
}
set inputValue(value) {
const {el} = this.searchBarInput;
if (el) {
el.value = value;
}
}
_onSearchInput() {
if (this.inputValue) {
this._openSearchMenu(this.inputValue);
this.inputValue = "";
}
}
_onSearchClick() {
this._openSearchMenu();
}
/**
* @param {String} [value]
* @private
*/
_openSearchMenu(value) {
const searchValue = value ? `/${value}` : "/";
this.command.openMainPalette({searchValue}, null);
}
}
AppsMenuOdooSearchBar.props = {};
AppsMenuOdooSearchBar.template = "web_responsive.AppsMenuOdooSearchBar";

View File

@@ -0,0 +1,4 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2021 ITerra - Sergey Shebanin
Copyright 2023 Onestein - Anjeel Haria
Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<!-- Search bar -->
<t t-name="web_responsive.AppsMenuOdooSearchBar">
<div class="search-container">
<div class="search-input">
<i class="fa fa-search search-icon fs-4 my-auto d-none d-sm-flex" />
<input
type="search"
t-ref="SearchBarInput"
t-on-input="_onSearchInput"
t-on-click="_onSearchClick"
autocomplete="off"
placeholder="Search menus..."
class="form-control"
/>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,26 @@
/** @odoo-module **/
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {AppsMenuCanonicalSearchBar} from "@web_responsive/components/menu_canonical_searchbar/searchbar.esm";
import {AppsMenuOdooSearchBar} from "@web_responsive/components/menu_odoo_searchbar/searchbar.esm";
import {AppsMenuFuseSearchBar} from "@web_responsive/components/menu_fuse_searchbar/searchbar.esm";
import {Component} from "@odoo/owl";
import {session} from "@web/session";
export class AppsMenuSearchBar extends Component {
setup() {
super.setup();
this.searchType = session.apps_menu.search_type || "canonical";
}
}
Object.assign(AppsMenuSearchBar, {
props: {},
template: "web_responsive.AppsMenuSearchBar",
components: {
AppsMenuOdooSearchBar,
AppsMenuCanonicalSearchBar,
AppsMenuFuseSearchBar,
},
});

View File

@@ -0,0 +1,45 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
.o_grid_apps_menu .search-container {
width: 100%;
.search-input {
display: flex;
justify-items: center;
gap: 0.75rem;
box-shadow: $app-menu-box-shadow;
border-radius: 4px;
padding: 0.5rem 0.75rem;
background-color: white;
.search-icon {
color: $app-menu-text-color;
font-size: 1.5rem;
padding-top: 1px;
}
.form-control {
height: 1.75rem;
background: none;
border: none;
color: $app-menu-text-color;
display: block;
padding: 0;
box-shadow: none;
&::placeholder {
color: $app-menu-text-color;
opacity: 0.5;
}
}
}
}
.o_command_palette_search .form-control {
&:focus {
box-shadow: unset;
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2021 ITerra - Sergey Shebanin
Copyright 2023 Onestein - Anjeel Haria
Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<!-- Search bar -->
<t t-name="web_responsive.AppsMenuSearchBar">
<AppsMenuCanonicalSearchBar t-if="searchType==='canonical'" />
<AppsMenuOdooSearchBar t-if="searchType==='command_palette'" />
<AppsMenuFuseSearchBar t-if="searchType==='fuse'" />
</t>
</templates>

View File

@@ -1,55 +0,0 @@
/** @odoo-module **/
/* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import SearchPanel from "@web/legacy/js/views/search_panel";
import {deviceContext} from "@web_responsive/components/ui_context.esm";
import {patch} from "web.utils";
// Patch search panel to add functionality for mobile view
patch(SearchPanel.prototype, "web_responsive.SearchPanelMobile", {
setup() {
this._super();
this.state.mobileSearch = false;
this.ui = deviceContext;
},
getActiveSummary() {
const selection = [];
for (const filter of this.model.get("sections")) {
let filterValues = [];
if (filter.type === "category") {
if (filter.activeValueId) {
const parentIds = this._getAncestorValueIds(
filter,
filter.activeValueId
);
filterValues = [...parentIds, filter.activeValueId].map(
(valueId) => filter.values.get(valueId).display_name
);
}
} else {
let values = [];
if (filter.groups) {
values = [
...[...filter.groups.values()].map((g) => g.values),
].flat();
}
if (filter.values) {
values = [...filter.values.values()];
}
filterValues = values
.filter((v) => v.checked)
.map((v) => v.display_name);
}
if (filterValues.length) {
selection.push({
values: filterValues,
icon: filter.icon,
color: filter.color,
type: filter.type,
});
}
}
return selection;
},
});

View File

@@ -1,112 +0,0 @@
/* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
.o_web_client {
.o_mobile_search {
position: fixed;
top: 0;
left: 0;
bottom: 0;
padding: 0;
width: 100%;
background-color: white;
z-index: $zindex-modal;
overflow: auto;
.o_mobile_search_header {
height: 46px;
margin-bottom: 10px;
width: 100%;
background-color: $o-brand-odoo;
color: white;
span:active {
background-color: darken($o-brand-primary, 10%);
}
span {
cursor: pointer;
}
}
.o_searchview_input_container {
display: flex;
padding: 15px 20px 0 20px;
position: relative;
.o_searchview_input {
width: 100%;
margin-bottom: 15px;
border-bottom: 1px solid $o-brand-secondary;
}
.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-bottom: 15%;
.o_dropdown {
width: 100%;
margin: 15px 5px 0px 5px;
border: solid 1px darken($gray-200, 20%);
}
.o_dropdown_toggler_btn {
width: 100%;
text-align: left;
&:after {
display: none;
}
}
// 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;
width: 100% !important;
transform: translate3d(0, 0, 0) !important;
box-shadow: none;
border: none;
color: $gray-600;
.divider {
margin: 0px;
}
> li > a {
padding: 10px 26px;
}
}
}
.o_mobile_search_show_result {
padding: 10px;
font-size: 15px;
}
}
}
// Search panel
@include media-breakpoint-down(sm) {
.o_controller_with_searchpanel {
display: block;
.o_search_panel {
height: auto;
padding: 8px;
border-left: 1px solid $gray-300;
section {
padding: 0px 16px;
}
}
.o_search_panel_summary {
cursor: pointer;
}
}
}

View File

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2021 Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t t-inherit="web.Legacy.SearchPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_search_panel')]" position="inside">
<div
t-if="ui.isSmall"
class="o_search_panel_summary"
t-on-click.stop="() => this.state.mobileSearch = true"
>
<div class="d-flex flex-wrap align-items-center">
<i class="fa fa-fw fa-filter mr-1" />
<t t-set="filters" t-value="getActiveSummary()" />
<span t-foreach="filters" t-as="filter" class="mx-1">
<i
t-if="filter.icon"
t-attf-class="fa {{ filter.icon }} mr-2"
t-att-style="filter.color and ('color: ' + filter.color)"
/>
<t
t-esc="filter.values.join(filter.type == 'category' ? ' / ' : ', ')"
/>
</span>
<t t-if="!filters.length">All</t>
</div>
</div>
<div
class="o_search_panel_content"
t-att-class="ui.isSmall ? (state.mobileSearch ? 'o_mobile_search' : 'd-none'): ''"
/>
</xpath>
<xpath expr="//div[hasclass('o_search_panel_content')]" position="inside">
<div t-if="ui.isSmall" class="o_mobile_search_header">
<span
class="o_mobile_search_close float-left mt16 mb16 mr8 ml16"
t-on-click.stop="state.mobileSearch = false"
>
<i class="fa fa-arrow-left" />
<strong class="float-right ml8">FILTER</strong>
</span>
</div>
<xpath expr="//section" position="move" />
<div
t-if="ui.isSmall"
class="btn btn-primary o_mobile_search_show_result fixed-bottom"
t-on-click.stop="state.mobileSearch = false"
>
<t>SEE RESULT</t>
</div>
</xpath>
</t>
</templates>

View File

@@ -1,47 +0,0 @@
/** @odoo-module **/
/* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {registry} from "@web/core/registry";
import {debounce} from "@web/core/utils/timing";
import config from "web.config";
import core from "web.core";
import Context from "web.Context";
// Legacy variant
// TODO: remove when legacy code will dropped from odoo
// TODO: then move context definition inside service start function
export const deviceContext = new Context({
isSmall: config.device.isMobile,
size: config.device.size_class,
SIZES: config.device.SIZES,
}).eval();
// New wowl variant
// TODO: use default odoo device context when it will be realized
const uiContextService = {
dependencies: ["ui"],
start(env, {ui}) {
window.addEventListener(
"resize",
debounce(() => {
const state = deviceContext;
if (state.size !== ui.size) {
state.size = ui.size;
}
if (state.isSmall !== ui.isSmall) {
state.isSmall = ui.isSmall;
config.device.isMobile = state.isSmall;
config.device.size_class = state.size;
core.bus.trigger("UI_CONTEXT:IS_SMALL_CHANGED");
}
}, 150) // UIService debounce for this event is 100
);
return deviceContext;
},
};
registry.category("services").add("ui_context", uiContextService);