Add oca-web submodule with 68 web modules

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ernad Husremovic 2025-08-30 17:27:15 +02:00
parent af56672c08
commit 53fddf87c8
2469 changed files with 101716 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,530 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="web-responsive">
<h1>Web Responsive</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:a44bfc2166613a6215ddd1033f08f81f4e7e8fdff325ab0dcfbb4ae27692c5ea
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/web/tree/16.0/web_responsive"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/web-16-0/web-16-0-web_responsive"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/web&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module adds responsiveness to web backend.</p>
<p><strong>Features for all devices</strong>:</p>
<ul>
<li><p class="first">New navigation with the fullscreen app menu</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/appmenu.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/appmenu.gif" />
</li>
<li><p class="first">Quick menu search inside the app menu</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/appsearch.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/appsearch.gif" />
</li>
<li><p class="first">Sticky header &amp; footer in list view</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/listview.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/listview.gif" />
</li>
<li><p class="first">Sticky statusbar in form view</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/formview.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/formview.gif" />
</li>
<li><p class="first">Bigger checkboxes in list view</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/listview.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/listview.gif" />
</li>
</ul>
<p><strong>Features for mobile</strong>:
* View type picker dropdown displays comfortably</p>
<blockquote>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/viewtype.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/viewtype.gif" />
</blockquote>
<ul>
<li><p class="first">Control panel buttons use icons to save space.</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/form_buttons.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/form_buttons.gif" />
</li>
<li><p class="first">Search panel is collapsed to mobile version on small screens.</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/search_panel.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/search_panel.gif" />
</li>
<li><p class="first">Followers and send button is displayed on mobile. Avatar is hidden.</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/chatter.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/chatter.gif" />
</li>
<li><p class="first">Big inputs on form in edit mode</p>
</li>
</ul>
<p><strong>Features for desktop computers</strong>:</p>
<ul>
<li><p class="first">Keyboard shortcuts for easier navigation,
<strong>using `Alt + Shift + [NUM]`</strong> combination instead of
just <cite>Alt + [NUM]</cite> to avoid conflict with Firefox Tab switching.
Standard Odoo keyboard hotkeys changed to be more intuitive or
accessible by fingers of one hand.
F.x. <cite>Alt + S</cite> for <cite>Save</cite></p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/shortcuts.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/shortcuts.gif" />
</li>
<li><p class="first">Autofocus on search menu box when opening the app menu</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/appsearch.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/appsearch.gif" />
</li>
<li><p class="first">Full width form sheets</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/formview.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/formview.gif" />
</li>
<li><p class="first">When the chatter is on the side part, the document viewer fills that
part for side-by-side reading instead of full screen. You can still put it on full
width preview clicking on the new maximize button.</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/document_viewer.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/document_viewer.gif" />
</li>
<li><p class="first">When the user chooses to send a public message the color of the composer is different
from the one when the message is an internal log.</p>
<img alt="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/chatter-colors.gif" src="https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/chatter-colors.gif" />
</li>
</ul>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<p>The following keyboard shortcuts are implemented:</p>
<ul class="simple">
<li>Navigate app search results - Arrow keys</li>
<li>Choose app result - <tt class="docutils literal">Enter</tt></li>
<li><tt class="docutils literal">Esc</tt> to close app drawer</li>
</ul>
</div>
<div class="section" id="known-issues-roadmap">
<h2><a class="toc-backref" href="#toc-entry-2">Known issues / Roadmap</a></h2>
<ul class="simple">
<li>App navigation with keyboard.</li>
<li>Handle long titles on forms in a better way</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_responsive%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-4">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-5">Authors</a></h3>
<ul class="simple">
<li>LasLabs</li>
<li>Tecnativa</li>
<li>ITerra</li>
<li>Onestein</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-6">Contributors</a></h3>
<ul class="simple">
<li>Dave Lasley &lt;<a class="reference external" href="mailto:dave&#64;laslabs.com">dave&#64;laslabs.com</a>&gt;</li>
<li>Jairo Llopis &lt;<a class="reference external" href="mailto:jairo.llopis&#64;tecnativa.com">jairo.llopis&#64;tecnativa.com</a>&gt;</li>
<li><dl class="first docutils">
<dt><a class="reference external" href="https://www.onestein.nl">Onestein</a>:</dt>
<dd><ul class="first last">
<li>Dennis Sluijk &lt;<a class="reference external" href="mailto:d.sluijk&#64;onestein.nl">d.sluijk&#64;onestein.nl</a>&gt;</li>
<li>Anjeel Haria</li>
</ul>
</dd>
</dl>
</li>
<li>Sergio Teruel &lt;<a class="reference external" href="mailto:sergio.teruel&#64;tecnativa.com">sergio.teruel&#64;tecnativa.com</a>&gt;</li>
<li>Alexandre Díaz &lt;<a class="reference external" href="mailto:dev&#64;redneboa.es">dev&#64;redneboa.es</a>&gt;</li>
<li>Mathias Markl &lt;<a class="reference external" href="mailto:mathias.markl&#64;mukit.at">mathias.markl&#64;mukit.at</a>&gt;</li>
<li>Iván Todorovich &lt;<a class="reference external" href="mailto:ivan.todorovich&#64;gmail.com">ivan.todorovich&#64;gmail.com</a>&gt;</li>
<li>Sergey Shebanin &lt;<a class="reference external" href="mailto:sergey&#64;shebanin.ru">sergey&#64;shebanin.ru</a>&gt;</li>
<li>David Vidal &lt;<a class="reference external" href="mailto:david.vidal&#64;tecnativa.com">david.vidal&#64;tecnativa.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/Tardo"><img alt="Tardo" src="https://github.com/Tardo.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/SplashS"><img alt="SplashS" src="https://github.com/SplashS.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/16.0/web_responsive">OCA/web</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View file

@ -0,0 +1,335 @@
/** @odoo-module **/
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
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;
// Patch WebClient to show AppsMenu instead of default app
patch(WebClient.prototype, "web_responsive.DefaultAppsMenu", {
setup() {
this._super();
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.menuService = useService("menu");
useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", () => {
this.setOpenState(false, false);
});
this._setupKeyNavigation();
}
setOpenState(open_state, from_home_menu_click) {
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);
}
}
/**
* Setup navigation among app menus
*/
_setupKeyNavigation() {
const repeatable = {
allowRepeat: true,
};
useHotkey(
"ArrowRight",
() => {
this._onWindowKeydown("next");
},
repeatable
);
useHotkey(
"ArrowLeft",
() => {
this._onWindowKeydown("prev");
},
repeatable
);
useHotkey(
"ArrowDown",
() => {
this._onWindowKeydown("next");
},
repeatable
);
useHotkey(
"ArrowUp",
() => {
this._onWindowKeydown("prev");
},
repeatable
);
useHotkey("Escape", () => {
this.env.bus.trigger("ACTION_MANAGER:UI-UPDATED");
});
}
_onWindowKeydown(direction) {
const focusableInputElements = document.querySelectorAll(`.o_app`);
if (focusableInputElements.length) {
const focusable = [...focusableInputElements];
const index = focusable.indexOf(document.activeElement);
let nextIndex = 0;
if (direction == "prev" && index >= 0) {
if (index > 0) {
nextIndex = index - 1;
} else {
nextIndex = focusable.length - 1;
}
} else if (direction == "next") {
if (index + 1 < focusable.length) {
nextIndex = index + 1;
} else {
nextIndex = 0;
}
}
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 [];
}
}
// 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;
},
});
AppsMenu.template = "web_responsive.AppsMenu";
AppsMenuSearchBar.template = "web_responsive.AppsMenuSearchResults";
Object.assign(NavBar.components, {AppsMenu, AppsMenuSearchBar});

View file

@ -0,0 +1,205 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
@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});
position: fixed;
margin: 0;
width: 100vw;
z-index: 1000;
left: 0 !important;
}
.o_apps_menu_opened .o_main_navbar {
.o_menu_brand,
.o_menu_sections {
display: none !important;
}
}
// Iconized full screen apps menu
.o_navbar_apps_menu {
.fade-enter-active,
.fade-leave-active {
transition: opacity 100ms ease;
}
.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);
}
}
.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;
}
}
}
}
}
.dropdown-menu-custom {
max-height: 70vh;
overflow: auto;
background-clip: border-box;
box-shadow: $o-dropdown-box-shadow;
}

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2021 ITerra - Sergey Shebanin
Copyright 2023 Onestein - Anjeel Haria
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 -->
<AppsMenu>
<AppsMenuSearchBar />
<DropdownItem
t-foreach="apps"
t-as="app"
t-key="app.id"
class="'o_app'"
dataset="{ menuXmlid: app.xmlid, section: app.id }"
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>
</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>
</div>
</div>
</t>
</templates>

View file

@ -0,0 +1,37 @@
/** @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

@ -0,0 +1,61 @@
/* 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) {
&:has(.o_AttachmentViewer) {
position: static;
}
.o_AttachmentViewer_main {
padding-bottom: 20px;
}
.o_AttachmentViewer {
// On-top of navbar
z-index: 1100;
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: 100% !important;
}
}
}
@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

@ -0,0 +1,39 @@
<?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,15 @@
/** @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

@ -0,0 +1,12 @@
/* Copyright 2025 Dixmit
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
.o_ChatterTopbar {
.o_ChatterTopbar_buttonSendMessage[small],
.o_ChatterTopbar_buttonLogNote[small] {
font-size: 0px;
i {
font-size: 1rem;
}
}
}

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2025 Dixmit
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates xml:space="preserve">
<!-- Moving some items from the chatter top bar for the mobile view -->
<t
t-name="web.Responsivemail.ChatterTopbar"
t-inherit="mail.ChatterTopbar"
owl="1"
t-inherit-mode="extension"
>
<xpath
expr="//button[hasclass('o_ChatterTopbar_buttonScheduleActivity')]/span "
position="attributes"
>
<attribute name="t-att-class">{
'd-none': ui.isSmall
}</attribute>
</xpath>
<xpath
expr="//button[hasclass('o_ChatterTopbar_buttonSendMessage')]"
position="inside"
>
<i
class="fa fa-paper-plane me-1"
role="img"
aria-label="Send message"
t-if="ui.isSmall"
/>
</xpath>
<xpath
expr="//button[hasclass('o_ChatterTopbar_buttonLogNote')]"
position="inside"
>
<i
class="fa fa-sticky-note-o me-1"
role="img"
aria-label="Log note"
t-if="ui.isSmall"
/>
</xpath>
<!-- We need to add an attribute to hide the text -->
<xpath
expr="//button[hasclass('o_ChatterTopbar_buttonSendMessage')]"
position="attributes"
>
<attribute name="t-att-small">ui.isSmall</attribute>
</xpath>
<xpath
expr="//button[hasclass('o_ChatterTopbar_buttonLogNote')]"
position="attributes"
>
<attribute name="t-att-small">ui.isSmall</attribute>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,45 @@
/** @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 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";
const {useState} = owl;
// 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.
// Patch legacy control panel to add states for mobile quick search
patch(LegacyControlPanel.prototype, "web_responsive.LegacyControlPanelMobile", {
setup() {
this._super();
this.state = useState({
mobileSearchMode: this.props.withBreadcrumbs ? "" : "quick",
});
this.ui = deviceContext;
},
setMobileSearchMode(ev) {
this.state.mobileSearchMode = ev.detail;
},
});
// 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

@ -0,0 +1,299 @@
/* 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_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

@ -0,0 +1,227 @@
<?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,12 @@
/* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
// Shortcut table ui improvement
.o_shortcut_table {
width: 100%;
white-space: nowrap;
max-width: 400px;
td {
padding: 0 20px;
}
}

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2021 ITerra - Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates id="template" xml:space="preserve">
<t t-inherit="web.NavBar.SectionsMenu" t-inherit-mode="extension" owl="1">
<xpath
expr="//t[@t-foreach='sections']//t[@t-set='hotkey']"
position="attributes"
>
<attribute
name="t-value"
t-translation="off"
>'shift+' + ((section_index + 1) % 10).toString()</attribute>
</xpath>
<xpath
expr="//t[@t-if='currentAppSectionsExtra.length']//t[@t-set='hotkey']"
position="attributes"
>
<attribute
name="t-value"
t-translation="off"
>'shift+' + (sectionsVisibleCount + 1 % 10).toString()</attribute>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,55 @@
/** @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

@ -0,0 +1,112 @@
/* 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

@ -0,0 +1,53 @@
<?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

@ -0,0 +1,47 @@
/** @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);

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2000" height="1128" viewBox="0 0 2000 1128">
<polygon fill-opacity=".03" points="0 1077.844 392.627 778.443 1504.99 1127.745 0 1127.745"/>
<polygon fill-opacity=".02" points="392.216 778.443 283.294 0 0 0 0 666.504"/>
<polygon fill-opacity=".03" points="1000 0 2000 1009.98 2000 439.94 1749.817 0"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View file

@ -0,0 +1,22 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define("web_responsive", function () {
"use strict";
// Fix for iOS Safari to set correct viewport height
// https://github.com/Faisal-Manzer/postcss-viewport-height-correction
function setViewportProperty(doc) {
function handleResize() {
requestAnimationFrame(function updateViewportHeight() {
doc.style.setProperty("--vh100", doc.clientHeight + "px");
});
}
handleResize();
return handleResize;
}
window.addEventListener(
"resize",
_.debounce(setViewportProperty(document.documentElement), 100)
);
});

View file

@ -0,0 +1,382 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* Copyright 2023 Onestein - Anjeel Haria
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
$chatter_zone_width: 35% !important;
// Scroll all but top bar
html .o_web_client .o_action_manager .o_action {
@include media-breakpoint-down(sm) {
overflow: auto;
.o_content {
overflow: visible !important;
}
}
max-width: 100%;
}
@include media-breakpoint-down(sm) {
.ui-menu .ui-menu-item {
height: 35px;
font-size: 15px;
}
.o_calendar_view .o_calendar_widget {
.fc-timeGridDay-view .fc-axis,
.fc-timeGridWeek-view .fc-axis {
padding-left: 0px;
}
.fc-dayGridYear-view {
padding-left: 0px;
> .fc-month-container {
width: 95%;
}
}
.fc-timeGridDay-view {
.fc-week-number {
padding: 0 4px;
width: 1em;
white-space: normal;
text-align: center;
}
.fc-day-header {
vertical-align: middle;
}
}
.fc-timeGridWeek-view .fc-widget-header {
word-spacing: 4em;
white-space: normal;
text-align: center;
}
}
.o_base_settings .o_setting_container {
display: block;
.settings_tab {
flex-flow: row nowrap;
padding-top: 0px;
.tab {
padding-right: 16px;
}
.selected {
background-color: #212529;
box-shadow: inset 0 -5px #7c7bad;
}
}
.settings > .app_settings_block .o_settings_container {
padding-left: 0;
padding-right: 0;
}
}
.o_kanban_view .o_control_panel .o_cp_bottom_right .o_cp_pager .btn-group {
top: -1px;
}
.o_kanban_renderer {
width: 100%;
}
}
// Normal views
.o_content,
.modal-content {
max-width: 100%;
// Form views
.o_form_editable {
.o_form_sheet {
max-width: calc(100% - 32px) !important;
overflow-x: auto;
}
.o_cell .o_form_label:not(.o_status):not(.o_calendar_invitation) {
min-height: 23px;
@include media-breakpoint-up(md) {
margin-bottom: 10px;
}
}
.o_horizontal_separator {
font-size: 14px;
}
// Some UX improvements for form in edit mode
@include media-breakpoint-down(sm) {
.nav-item {
border-bottom: 1px solid #dee2e6;
}
.nav-tabs {
border-bottom: none;
}
&.o_form_editable .o_field_widget {
&:not(.o_stat_info):not(.o_readonly_modifier):not(.oe_form_field_html):not(.o_field_image) {
min-height: 35px;
}
.o_x2m_control_panel {
margin-bottom: 10px;
}
&.o_field_float_percentage,
&.o_field_monetary,
&.o_field_many2many_selection,
.o_field_many2one_selection {
align-items: center;
}
.o_field_many2one_selection .o_input_dropdown,
&.o_datepicker,
&.o_partner_autocomplete_info {
input {
min-height: 35px;
}
}
.o_external_button {
margin-left: 10px;
}
.o_dropdown_button,
.o_datepicker_button {
top: 8px;
right: 6px;
bottom: auto;
}
.o_field_many2many_selection .o_dropdown_button {
top: 0 !important;
}
}
}
// Sticky statusbar
.o_form_statusbar {
position: sticky !important;
top: 0;
z-index: 2;
}
// Support for long title (with ellipsis)
.oe_title {
.o_field_widget:not(.oe_inline) {
span:not(.o_field_translate):not(.o_status) {
display: block;
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: initial;
&:active {
white-space: normal;
}
}
}
}
@include media-breakpoint-down(sm) {
min-width: auto;
// More buttons border
.oe_button_box {
.o_dropdown_more {
button:last-child {
border-right: 1px solid $gray-400 !important;
}
}
}
.oe_button_box + .oe_title,
.oe_button_box + .oe_avatar + .oe_title {
width: 100%;
}
// Avoid overflow on modals
.o_form_sheet {
min-width: auto;
}
// Render website inputs properly in phones
.o_group .o_field_widget.o_text_overflow {
// Overrides another !important
width: auto !important;
}
// Full width in form sheets
.o_form_sheet,
.o_FormRenderer_chatterContainer {
min-width: auto;
max-width: 98% !important;
}
// Settings pages
.app_settings_block {
.row {
margin: 0;
}
}
.o_FormRenderer_chatterContainer {
padding-top: initial;
// Display send button on small screens
.o_Chatter_composer {
&.o-has-current-partner-avatar {
grid-template-columns: 0px 1fr;
padding: 1rem 1rem 1.5rem 1rem !important;
}
.o_Composer_sidebarMain {
display: none;
}
}
}
}
}
//No content message improvements on mobile
@include media-breakpoint-down(md) {
.o_view_nocontent {
top: 80px;
}
.o_nocontent_help {
box-shadow: none;
}
.o_sample_data_disabled {
display: none;
}
}
}
// Sticky Header & Footer in List View
.o_list_view {
.table-responsive {
.o_list_table {
// th & td are here for compatibility with chrome
thead tr:nth-child(1) th {
position: sticky !important;
top: 0;
z-index: 999;
}
thead tr:nth-child(1) th {
background-color: var(--ListRenderer-thead-bg-color);
border-top: none !important;
border-bottom: none !important;
border-left: transparent;
box-shadow: inset 0 0 0 $o-community-color,
inset 0 -1px 0 $o-community-color;
}
tfoot,
tfoot tr:nth-child(1) td {
position: sticky;
bottom: 0;
}
tfoot tr:nth-child(1) td {
background-color: $o-list-footer-bg-color;
border-top: none !important;
border-bottom: none !important;
box-shadow: inset 0 1px 0 $o-community-color,
inset 0 0 0 $o-community-color;
}
}
.table {
thead tr:nth-child(1) th {
z-index: 1;
}
}
}
}
// Big checkboxes
.o_list_view,
.o_setting_container .o_setting_box {
.o_setting_right_pane {
margin-left: 34px;
}
.o-checkbox:not(.o_boolean_toggle) {
margin-right: 10px;
margin-top: -6px;
&.d-inline-block {
display: block !important;
}
.form-check-input {
height: 24px;
width: 24px;
}
}
.o_optional_columns_dropdown,
.o_add_favorite {
.o-checkbox {
margin-top: 0;
}
.form-check-input {
height: 1em !important;
width: 1em !important;
}
}
}
.o_setting_container .o_setting_box {
.o_setting_right_pane {
margin-left: 34px;
}
.o-checkbox:not(.o_boolean_toggle) {
margin-right: 10px;
.form-check-label {
&::after {
width: 24px;
height: 24px;
}
&::before {
outline: none !important;
border: 1px solid #4c4c4c;
width: 24px;
height: 24px;
}
}
}
}
.o_chatter_header_container {
padding-top: $grid-gutter-width * 0.5;
top: 0;
position: sticky;
background-color: $o-view-background-color;
z-index: 1;
}
.o_FormRenderer_chatterContainer {
&.o-isInFormSheetBg:not(.o-aside) {
background-color: $white;
&:not(.o-aside) {
width: auto;
border-top: 1px solid $border-color;
}
}
&.o-aside {
flex: 0 0 $chatter_zone_width;
max-width: initial;
min-width: initial;
overflow: auto;
}
}
body:not(.o_statusbar_buttons) {
.oe-toolbar {
z-index: 0;
}
}
.o_inner_group > .mb-sm-0 {
margin-bottom: 0 !important;
}
.w-26 {
width: 26%;
}
// Color clue to tell the difference between a note and a public message
.o_Chatter_composer {
// HACK: has() pseudo class is broadly supported in desktop, even FF will deploy
// full support soon (now it's available behind a config flag)
// https://caniuse.com/css-has
&:has(div.o_Composer_coreHeader) {
background-color: lighten($o-brand-primary, 40%);
}
}
.o_searchview_autocomplete {
z-index: 999;
}

View file

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017 LasLabs Inc.
Copyright 2018 Alexandre Díaz
Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2021 ITerra - Sergey Shebanin
Copyright 2023 Onestein - Anjeel Haria
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates id="form_view" xml:space="preserve">
<!-- Template for buttons that display only the icon in xs -->
<t t-name="web_responsive.icon_button_create" owl="1">
<i t-attf-class="fa fa-plus" title="New" />
<span class="d-none d-sm-inline"> New</span>
</t>
<t t-name="web_responsive.icon_button_save" owl="1">
<i t-attf-class="fa fa-check" title="Save" />
<span class="d-none d-sm-inline"> Save</span>
</t>
<t t-name="web_responsive.icon_button_discard" owl="1">
<i t-attf-class="fa fa-times" title="Discard" />
<span class="d-none d-sm-inline"> Discard</span>
</t>
<t
t-name="web.ResponsiveFormView.Buttons"
t-inherit="web.FormView.Buttons"
owl="1"
t-inherit-mode="extension"
>
<!-- Change "Discard" button hotkey to "D" -->
<xpath
expr="//button[contains(@class, 'o_form_button_cancel')]"
position="attributes"
>
<attribute name="data-hotkey">d</attribute>
</xpath>
<xpath
expr="//button[contains(@class, 'o_form_button_create')]"
position="replace"
>
<button
type="button"
class="btn btn-secondary o_form_button_create"
data-hotkey="c"
t-on-click.stop="create"
>
<t t-call="web_responsive.icon_button_create" />
</button>
</xpath>
</t>
<t
t-name="web.ResponsiveFormView"
t-inherit="web.FormView"
owl="1"
t-inherit-mode="extension"
>
<xpath
expr="//button[contains(@class, 'o_form_button_create')]"
position="replace"
>
<button
type="button"
class="btn btn-outline-primary o_form_button_create"
data-hotkey="c"
t-on-click.stop="create"
t-att-disabled="state.isDisabled"
>
<t t-call="web_responsive.icon_button_create" />
</button>
</xpath>
</t>
<t
t-name="web.ResponsiveFormStatusIndicator"
t-inherit="web.FormStatusIndicator"
owl="1"
>
<!-- Change "Discard" button hotkey to "D" -->
<xpath
expr="//button[contains(@class, 'o_form_button_cancel')]"
position="attributes"
>
<attribute name="data-hotkey">d</attribute>
</xpath>
</t>
<t
t-name="web.ResponsiveKanbanView.Buttons"
t-inherit="web.KanbanView.Buttons"
owl="1"
t-inherit-mode="extension"
>
<!-- Add responsive icons to buttons -->
<xpath
expr="//button[contains(@class, 'o-kanban-button-new')]"
position="replace"
>
<button
type="button"
class="btn btn-primary o-kanban-button-new"
accesskey="c"
t-on-click="() => this.createRecord(null)"
data-bounce-button=""
>
<t t-call="web_responsive.icon_button_create" />
</button>
</xpath>
</t>
<t
t-name="web.ResponsiveListView.Buttons"
t-inherit="web.ListView.Buttons"
owl="1"
t-inherit-mode="extension"
>
<!-- Add responsive icons to buttons -->
<xpath
expr="//button[contains(@class, 'o_list_button_add')]"
position="replace"
>
<button
type="button"
class="btn btn-primary o_list_button_add"
data-hotkey="c"
t-on-click="onClickCreate"
data-bounce-button=""
>
<t t-call="web_responsive.icon_button_create" />
</button>
</xpath>
<xpath
expr="//button[contains(@class, 'o_list_button_save')]"
position="replace"
>
<button
type="button"
class="btn btn-primary o_list_button_save"
data-hotkey="s"
t-on-click.stop="onClickSave"
>
<t t-call="web_responsive.icon_button_save" />
</button>
</xpath>
<xpath
expr="//button[contains(@class, 'o_list_button_discard')]"
position="replace"
>
<button
type="button"
class="btn btn-secondary o_list_button_discard"
data-hotkey="d"
t-on-click="onClickDiscard"
t-on-mousedown="onMouseDownDiscard"
>
<t t-call="web_responsive.icon_button_discard" />
</button>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,14 @@
/** @odoo-module */
/* Copyright 2023 Onestein - Anjeel Haria
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {patch} from "@web/core/utils/patch";
import {FormController} from "@web/views/form/form_controller";
// Patch FormController to always load attachment alongwith the chatter on the side bar
patch(FormController.prototype, "web_responsive.FormController", {
setup() {
this._super();
this.hasAttachmentViewerInArch = false;
},
});

View file

@ -0,0 +1,9 @@
/* Copyright 2023 Tecnativa - Carlos Roca
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
// Many2one li items with wrap
.o_field_many2one_selection {
ul.ui-autocomplete .dropdown-item.ui-menu-item-wrapper {
white-space: initial;
}
}

View file

@ -0,0 +1,19 @@
/* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define("web_responsive.test_patch", function (require) {
"use strict";
const utils = require("web_tour.TourStepUtils");
/* Make base odoo JS tests working */
utils.include({
showAppsMenuItem() {
return {
edition: "community",
trigger: ".o_navbar_apps_menu",
auto: true,
position: "bottom",
};
},
});
});