3
0
Fork 0

[MIG] web_responsive: Migration to 17.0

17.0
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

@ -17,13 +17,13 @@ Web Responsive
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/16.0/web_responsive
:target: https://github.com/OCA/web/tree/17.0/web_responsive
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-16-0/web-16-0-web_responsive
:target: https://translation.odoo-community.org/projects/web-17-0/web-17-0-web_responsive
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=16.0
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=17.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
@ -32,75 +32,76 @@ This module adds responsiveness to web backend.
**Features for all devices**:
* New navigation with the fullscreen app menu
- New navigation with the fullscreen app menu
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/appmenu.gif
|image|
* Quick menu search inside the app menu
- Quick menu search inside the app menu
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/appsearch.gif
|image1|
* Sticky header & footer in list view
- Sticky header & footer in list view
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/listview.gif
|image2|
* Sticky statusbar in form view
- Sticky statusbar in form view
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/formview.gif
|image3|
* Bigger checkboxes in list view
- Bigger checkboxes in list view
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/listview.gif
|image4|
**Features for mobile**: \* View type picker dropdown displays
comfortably
**Features for mobile**:
* View type picker dropdown displays comfortably
- Control panel buttons use icons to save space.
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/viewtype.gif
|image5|
* Control panel buttons use icons to save space.
- Followers and send button is displayed on mobile. Avatar is hidden.
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/form_buttons.gif
|image6|
* Search panel is collapsed to mobile version on small screens.
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/search_panel.gif
* Followers and send button is displayed on mobile. Avatar is hidden.
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/chatter.gif
* Big inputs on form in edit mode
- Big inputs on form in edit mode
**Features for desktop computers**:
* Keyboard shortcuts for easier navigation,
**using `Alt + Shift + [NUM]`** combination instead of
just `Alt + [NUM]` 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. `Alt + S` for `Save`
- Keyboard shortcuts for easier navigation, **using \`Alt + Shift +
[NUM]\`** combination instead of just Alt + [NUM] 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. Alt + S
for Save
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/shortcuts.gif
|image7|
* Autofocus on search menu box when opening the app menu
- Autofocus on search menu box when opening the app menu
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/appsearch.gif
|image8|
* Full width form sheets
- 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.
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/formview.gif
|image9|
* 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.
- 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.
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/document_viewer.gif
|image10|
* 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.
.. image:: https://raw.githubusercontent.com/OCA/web/16.0/web_responsive/static/img/chatter-colors.gif
.. |image| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/appmenu.gif
.. |image1| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/appsearch.gif
.. |image2| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/listview.gif
.. |image3| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/formview.gif
.. |image4| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/listview.gif
.. |image5| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/form_buttons.gif
.. |image6| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/chatter.png
.. |image7| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/shortcuts.gif
.. |image8| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/appsearch.gif
.. |image9| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/document_viewer.gif
.. |image10| image:: https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/chatter-colors.png
**Table of contents**
@ -112,15 +113,15 @@ Usage
The following keyboard shortcuts are implemented:
* Navigate app search results - Arrow keys
* Choose app result - ``Enter``
* ``Esc`` to close app drawer
- Navigate app search results - Arrow keys
- Choose app result - ``Enter``
- ``Esc`` to close app drawer
Known issues / Roadmap
======================
* App navigation with keyboard.
* Handle long titles on forms in a better way
- App navigation with keyboard.
- Handle long titles on forms in a better way
Bug Tracker
===========
@ -128,7 +129,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
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
`feedback <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 <https://github.com/OCA/web/issues/new?body=module:%20web_responsive%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
@ -136,7 +137,7 @@ Credits
=======
Authors
~~~~~~~
-------
* LasLabs
* Tecnativa
@ -144,22 +145,33 @@ Authors
* Onestein
Contributors
~~~~~~~~~~~~
------------
* Dave Lasley <dave@laslabs.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
* `Onestein <https://www.onestein.nl>`_:
* Dennis Sluijk <d.sluijk@onestein.nl>
* Anjeel Haria
* Sergio Teruel <sergio.teruel@tecnativa.com>
* Alexandre Díaz <dev@redneboa.es>
* Mathias Markl <mathias.markl@mukit.at>
* Iván Todorovich <ivan.todorovich@gmail.com>
* Sergey Shebanin <sergey@shebanin.ru>
* David Vidal <david.vidal@tecnativa.com>
- Dave Lasley <dave@laslabs.com>
- Jairo Llopis <jairo.llopis@tecnativa.com>
- `Onestein <https://www.onestein.nl>`__:
- Dennis Sluijk <d.sluijk@onestein.nl>
- Anjeel Haria
- Sergio Teruel <sergio.teruel@tecnativa.com>
- Alexandre Díaz <dev@redneboa.es>
- Mathias Markl <mathias.markl@mukit.at>
- Iván Todorovich <ivan.todorovich@gmail.com>
- Sergey Shebanin <sergey@shebanin.ru>
- David Vidal <david.vidal@tecnativa.com>
- Taras Shabaranskyi <shabaranskij@gmail.com>
Maintainers
~~~~~~~~~~~
-----------
This module is maintained by the OCA.
@ -185,6 +197,6 @@ Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-Yajo| |maintainer-Tardo| |maintainer-SplashS|
This module is part of the `OCA/web <https://github.com/OCA/web/tree/16.0/web_responsive>`_ project on GitHub.
This module is part of the `OCA/web <https://github.com/OCA/web/tree/17.0/web_responsive>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,4 @@
# Copyright 2023 Taras Shabaranskyi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import models

View File

@ -3,12 +3,13 @@
# Copyright 2018-2019 Tecnativa - Alexandre Díaz
# 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.html).
{
"name": "Web Responsive",
"summary": "Responsive web client, community-supported",
"version": "16.0.1.2.3",
"version": "17.0.1.0.0",
"category": "Website",
"website": "https://github.com/OCA/web",
"author": "LasLabs, Tecnativa, ITerra, Onestein, "
@ -19,35 +20,45 @@
"development_status": "Production/Stable",
"maintainers": ["Yajo", "Tardo", "SplashS"],
"excludes": ["web_enterprise"],
"data": ["views/web.xml"],
"data": [
"views/res_users_views.xml",
],
"assets": {
"web._assets_primary_variables": {
"/web_responsive/static/src/legacy/scss/form_variable.scss",
"/web_responsive/static/src/legacy/scss/primary_variable.scss",
},
"web.assets_backend": [
"/web_responsive/static/src/views/form/form_controller.esm.js",
"web_responsive/static/src/lib/fuse/fuse.basic.min.js",
"/web_responsive/static/src/legacy/scss/web_responsive.scss",
"/web_responsive/static/src/legacy/js/web_responsive.js",
"/web_responsive/static/src/components/ui_context.esm.js",
"/web_responsive/static/src/components/apps_menu/apps_menu.scss",
"/web_responsive/static/src/components/apps_menu/apps_menu.esm.js",
"/web_responsive/static/src/components/control_panel/control_panel.scss",
"/web_responsive/static/src/components/control_panel/control_panel.esm.js",
"/web_responsive/static/src/components/search_panel/search_panel.scss",
"/web_responsive/static/src/components/search_panel/search_panel.esm.js",
"/web_responsive/static/src/components/hotkey/hotkey.scss",
"/web_responsive/static/src/legacy/scss/big_boxes.scss",
"/web_responsive/static/src/legacy/scss/list_sticky_header.scss",
"/web_responsive/static/src/legacy/js/web_responsive.esm.js",
"/web_responsive/static/src/legacy/xml/form_buttons.xml",
"/web_responsive/static/src/components/apps_menu/apps_menu.xml",
"/web_responsive/static/src/components/control_panel/control_panel.xml",
"/web_responsive/static/src/components/search_panel/search_panel.xml",
"/web_responsive/static/src/components/hotkey/hotkey.xml",
"/web_responsive/static/src/components/chatter_topbar/chatter_topbar.esm.js",
"/web_responsive/static/src/components/chatter_topbar/chatter_topbar.xml",
"/web_responsive/static/src/components/attachment_viewer/attachment_viewer.scss",
"/web_responsive/static/src/components/attachment_viewer/attachment_viewer.esm.js",
"/web_responsive/static/src/components/attachment_viewer/attachment_viewer.xml",
"/web_responsive/static/src/legacy/xml/custom_favorite_item.xml",
"/web_responsive/static/src/components/apps_menu_tools.esm.js",
"/web_responsive/static/src/components/apps_menu/*",
"/web_responsive/static/src/components/apps_menu_item/*",
"/web_responsive/static/src/components/menu_canonical_searchbar/*",
"/web_responsive/static/src/components/menu_odoo_searchbar/*",
"/web_responsive/static/src/components/menu_fuse_searchbar/*",
"/web_responsive/static/src/components/menu_searchbar/*",
"/web_responsive/static/src/components/hotkey/*",
"/web_responsive/static/src/components/file_viewer/*",
"/web_responsive/static/src/components/chatter/*",
"/web_responsive/static/src/components/control_panel/*",
"/web_responsive/static/src/components/command_palette/*",
"/web_responsive/static/src/views/form/form_controller.scss",
"/web_responsive/static/src/views/form/status_bar_buttons.xml",
"/web_responsive/static/src/views/form/form_statusbar.scss",
],
"web.assets_tests": [
"/web_responsive/static/tests/test_patch.js",
],
"web.qunit_suite_tests": [
"/web_responsive/static/tests/apps_menu_tests.esm.js",
"/web_responsive/static/tests/apps_menu_search_tests.esm.js",
],
},
"sequence": 1,
}

View File

@ -0,0 +1,105 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * web_responsive
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0-20231123\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-11-25 15:27+0000\n"
"PO-Revision-Date: 2023-11-25 15:27+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/views/form/status_bar_buttons.xml:0
#, python-format
msgid "Action"
msgstr ""
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/components/apps_menu_searchbar/apps_menu_searchbar.xml:0
#, python-format
msgid "App Icon"
msgstr ""
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/legacy/xml/form_buttons.xml:0
#: code:addons/web_responsive/static/src/legacy/xml/form_buttons.xml:0
#, python-format
msgid "Discard"
msgstr ""
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/components/apps_menu/apps_menu.xml:0
#, python-format
msgid "Home Menu"
msgstr "Головне меню"
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/components/chatter/chatter.xml:0
#, python-format
msgid "Log note"
msgstr ""
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/components/file_viewer/file_viewer.xml:0
#: code:addons/web_responsive/static/src/components/file_viewer/file_viewer.xml:0
#, python-format
msgid "Maximize"
msgstr "Збільшити"
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/components/file_viewer/file_viewer.xml:0
#: code:addons/web_responsive/static/src/components/file_viewer/file_viewer.xml:0
#, python-format
msgid "Minimize"
msgstr "Згорнути"
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/legacy/xml/form_buttons.xml:0
#: code:addons/web_responsive/static/src/legacy/xml/form_buttons.xml:0
#, python-format
msgid "New"
msgstr "Новий"
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/components/apps_menu_searchbar/apps_menu_searchbar.xml:0
#, python-format
msgid "Nothing to show"
msgstr "Нема чого показати"
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/legacy/xml/form_buttons.xml:0
#: code:addons/web_responsive/static/src/legacy/xml/form_buttons.xml:0
#, python-format
msgid "Save"
msgstr "Зберегти"
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/components/apps_menu_searchbar/apps_menu_searchbar.xml:0
#, python-format
msgid "Search menus..."
msgstr "Пошук..."
#. module: web_responsive
#. odoo-javascript
#: code:addons/web_responsive/static/src/components/chatter/chatter.xml:0
#, python-format
msgid "Send message"
msgstr "Надіслати повідомлення"

View File

@ -0,0 +1,5 @@
# Copyright 2023 Taras Shabaranskyi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import res_users
from . import ir_http

View File

@ -0,0 +1,19 @@
# Copyright 2023 Taras Shabaranskyi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import models
class IrHttp(models.AbstractModel):
_inherit = "ir.http"
def session_info(self):
session = super().session_info()
user = self.env.user
return {
**session,
"apps_menu": {
"search_type": user.apps_menu_search_type,
"theme": user.apps_menu_theme,
},
}

View File

@ -0,0 +1,26 @@
# Copyright 2023 Taras Shabaranskyi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class ResUsers(models.Model):
_inherit = "res.users"
apps_menu_search_type = fields.Selection(
[
("canonical", "Canonical"),
("fuse", "Fuse"),
("command_palette", "Command Palette"),
],
default="canonical",
required=True,
)
apps_menu_theme = fields.Selection(
[
("milk", "Milk"),
("community", "Community"),
],
default="milk",
required=True,
)

View File

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@ -0,0 +1,21 @@
- Dave Lasley \<<dave@laslabs.com>\>
- Jairo Llopis \<<jairo.llopis@tecnativa.com>\>
- [Onestein](https://www.onestein.nl):
- Dennis Sluijk \<<d.sluijk@onestein.nl>\>
- Anjeel Haria
- Sergio Teruel \<<sergio.teruel@tecnativa.com>\>
- Alexandre Díaz \<<dev@redneboa.es>\>
- Mathias Markl \<<mathias.markl@mukit.at>\>
- Iván Todorovich \<<ivan.todorovich@gmail.com>\>
- Sergey Shebanin \<<sergey@shebanin.ru>\>
- David Vidal \<<david.vidal@tecnativa.com>\>
- Taras Shabaranskyi \<<shabaranskij@gmail.com>\>

View File

@ -1,11 +0,0 @@
* Dave Lasley <dave@laslabs.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
* `Onestein <https://www.onestein.nl>`_:
* Dennis Sluijk <d.sluijk@onestein.nl>
* Anjeel Haria
* Sergio Teruel <sergio.teruel@tecnativa.com>
* Alexandre Díaz <dev@redneboa.es>
* Mathias Markl <mathias.markl@mukit.at>
* Iván Todorovich <ivan.todorovich@gmail.com>
* Sergey Shebanin <sergey@shebanin.ru>
* David Vidal <david.vidal@tecnativa.com>

View File

@ -0,0 +1,62 @@
This module adds responsiveness to web backend.
**Features for all devices**:
- New navigation with the fullscreen app menu
![image](../static/img/appmenu.gif)
- Quick menu search inside the app menu
![image](../static/img/appsearch.gif)
- Sticky header & footer in list view
![image](../static/img/listview.gif)
- Sticky statusbar in form view
![image](../static/img/formview.gif)
- Bigger checkboxes in list view
![image](../static/img/listview.gif)
**Features for mobile**: \* View type picker dropdown displays
comfortably
- Control panel buttons use icons to save space.
![image](../static/img/form_buttons.gif)
- Followers and send button is displayed on mobile. Avatar is hidden.
![image](../static/img/chatter.png)
- Big inputs on form in edit mode
**Features for desktop computers**:
- Keyboard shortcuts for easier navigation, **using \`Alt + Shift +
\[NUM\]\`** combination instead of just Alt + \[NUM\] 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. Alt + S for Save
![image](../static/img/shortcuts.gif)
- Autofocus on search menu box when opening the app menu
![image](../static/img/appsearch.gif)
- 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.
![image](../static/img/document_viewer.gif)
- 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.
![image](../static/img/chatter-colors.png)

View File

@ -1,73 +0,0 @@
This module adds responsiveness to web backend.
**Features for all devices**:
* New navigation with the fullscreen app menu
.. image:: ../static/img/appmenu.gif
* Quick menu search inside the app menu
.. image:: ../static/img/appsearch.gif
* Sticky header & footer in list view
.. image:: ../static/img/listview.gif
* Sticky statusbar in form view
.. image:: ../static/img/formview.gif
* Bigger checkboxes in list view
.. image:: ../static/img/listview.gif
**Features for mobile**:
* View type picker dropdown displays comfortably
.. image:: ../static/img/viewtype.gif
* Control panel buttons use icons to save space.
.. image:: ../static/img/form_buttons.gif
* Search panel is collapsed to mobile version on small screens.
.. image:: ../static/img/search_panel.gif
* Followers and send button is displayed on mobile. Avatar is hidden.
.. image:: ../static/img/chatter.gif
* Big inputs on form in edit mode
**Features for desktop computers**:
* Keyboard shortcuts for easier navigation,
**using `Alt + Shift + [NUM]`** combination instead of
just `Alt + [NUM]` 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. `Alt + S` for `Save`
.. image:: ../static/img/shortcuts.gif
* Autofocus on search menu box when opening the app menu
.. image:: ../static/img/appsearch.gif
* Full width form sheets
.. image:: ../static/img/formview.gif
* 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.
.. image:: ../static/img/document_viewer.gif
* 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.
.. image:: ../static/img/chatter-colors.gif

View File

@ -0,0 +1,2 @@
- App navigation with keyboard.
- Handle long titles on forms in a better way

View File

@ -1,2 +0,0 @@
* App navigation with keyboard.
* Handle long titles on forms in a better way

View File

@ -0,0 +1,5 @@
The following keyboard shortcuts are implemented:
- Navigate app search results - Arrow keys
- Choose app result - `Enter`
- `Esc` to close app drawer

View File

@ -1,5 +0,0 @@
The following keyboard shortcuts are implemented:
* Navigate app search results - Arrow keys
* Choose app result - ``Enter``
* ``Esc`` to close app drawer

View File

@ -369,68 +369,59 @@ ul.auto-toc {
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:9b3ae1467041b443396d6062ed0af40d96c2fa5e97cbce6b17e7daa93a3ee53f
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<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/licence-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><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/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/web/tree/17.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-17-0/web-17-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=17.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" />
<p><img alt="image" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/appmenu.gif" /></p>
</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" />
<p><img alt="image1" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/appsearch.gif" /></p>
</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" />
<p><img alt="image2" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/listview.gif" /></p>
</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" />
<p><img alt="image3" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/formview.gif" /></p>
</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" />
<p><img alt="image4" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/listview.gif" /></p>
</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>
<p><strong>Features for mobile</strong>: * View type picker dropdown displays
comfortably</p>
<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" />
<p><img alt="image5" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/form_buttons.gif" /></p>
</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" />
<p><img alt="image6" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/chatter.png" /></p>
</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><p class="first">Keyboard shortcuts for easier navigation, <strong>using `Alt + Shift +
[NUM]`</strong> combination instead of just Alt + [NUM] 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. Alt + S
for Save</p>
<p><img alt="image7" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/shortcuts.gif" /></p>
</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" />
<p><img alt="image8" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/appsearch.gif" /></p>
</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" />
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>
<p><img alt="image9" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/document_viewer.gif" /></p>
</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><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>
<p><img alt="image10" src="https://raw.githubusercontent.com/OCA/web/17.0/web_responsive/static/img/chatter-colors.png" /></p>
</li>
</ul>
<p><strong>Table of contents</strong></p>
@ -468,7 +459,7 @@ from the one when the message is an internal log.</p>
<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>
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_responsive%0Aversion:%2017.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">
@ -487,14 +478,10 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
<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><a class="reference external" href="https://www.onestein.nl">Onestein</a>:<ul>
<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>
@ -502,6 +489,7 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
<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>
<li>Taras Shabaranskyi &lt;<a class="reference external" href="mailto:shabaranskij&#64;gmail.com">shabaranskij&#64;gmail.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
@ -513,7 +501,7 @@ 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/Yajo"><img alt="Yajo" src="https://github.com/Yajo.png?size=40px" /></a> <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>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/17.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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

After

Width:  |  Height:  |  Size: 837 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

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);

View File

@ -0,0 +1,27 @@
/** @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 {debounce} from "@web/core/utils/timing";
// Fix for iOS Safari to set correct viewport height
// https://github.com/Faisal-Manzer/postcss-viewport-height-correction
export function setViewportProperty(doc) {
function handleResize() {
requestAnimationFrame(function () {
doc.style.setProperty("--vh100", doc.clientHeight + "px");
});
}
handleResize();
return handleResize;
}
window.addEventListener(
"resize",
debounce(setViewportProperty(document.documentElement), 25)
);

View File

@ -1,22 +0,0 @@
/* 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,95 @@
/* 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). */
$big-checkbox-size: 1.5em;
// 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: $big-checkbox-size;
width: $big-checkbox-size;
}
}
.o_optional_columns_dropdown {
.o-dropdown--menu {
display: flex !important;
flex-direction: column;
margin: 0;
}
.o-checkbox {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.form-check-input {
margin-bottom: 2px;
}
}
}
.o_add_favorite + .o_accordion_values {
.o_add_favorite_props {
display: flex;
flex-direction: column;
margin: 0;
}
.o_add_favorite_name {
margin-bottom: 0.5rem;
max-width: 100%;
}
.form-check-input {
height: $big-checkbox-size;
width: $big-checkbox-size;
}
.form-check-label {
line-height: normal;
}
.o-checkbox {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
}
.o_setting_container .o_setting_box {
.o-checkbox:not(.o_boolean_toggle) {
.form-check-label {
&::after {
width: 24px;
height: 24px;
}
&::before {
outline: none !important;
border: 1px solid #4c4c4c;
width: 24px;
height: 24px;
}
}
}
}

View File

@ -0,0 +1,5 @@
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
$o-form-renderer-max-width: 3840px;
$o-form-view-sheet-max-width: 2560px;

View File

@ -0,0 +1,26 @@
/* 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). */
.o_mobile_sticky {
transition: top 0.5s;
}
// Sticky Header & Footer in List View
.o_list_view {
.o_list_table {
thead {
box-shadow: 0 1px 0 0 var(--ListRenderer-thead-border-end-color);
}
.o_list_footer {
position: sticky;
bottom: 0;
z-index: 2;
background-color: var(--ListRenderer-thead-bg-color);
box-shadow: 0 -1px 0 -1px var(--ListRenderer-thead-border-end-color);
}
}
}

View File

@ -0,0 +1,12 @@
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
$app-menu-text-color: #374151 !default;
$app-menu-background-color: rgb(233, 230, 249) !default;
$app-menu-item-highlight: rgb(243, 240, 259) !default;
$app-menu-box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.16),
0 2px 2px rgba(0, 0, 0, 0.016), 0 4px 4px rgba(0, 0, 0, 0.016),
0 8px 8px rgba(0, 0, 0, 0.016), 0 16px 16px rgba(0, 0, 0, 0.016) !default;
$app-menu-box-shadow-highlight: inset 0 0 0 1px rgba(0, 0, 0, 0.26),
0 2px 2px rgba(0, 0, 0, 0.026), 0 4px 4px rgba(0, 0, 0, 0.026),
0 8px 8px rgba(0, 0, 0, 0.026), 0 16px 16px rgba(0, 0, 0, 0.026) !default;

View File

@ -1,89 +1,42 @@
/* 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). */
$chatter_zone_width: 35% !important;
// Support for long titles
@include media-breakpoint-up(md) {
.o_form_view .oe_button_box + .oe_title,
.o_form_view .oe_button_box + .oe_avatar + .oe_title {
/* Button-box has a hardcoded width of 132px per button and have three columns */
width: calc(100% - 450px);
}
}
// Allow sticky header
.o_action_manager {
.o_form_view {
overflow: unset;
// 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;
.o_form_view_container {
overflow: auto;
}
}
max-width: 100%;
}
@include media-breakpoint-down(sm) {
.ui-menu .ui-menu-item {
.ui-menu-item-wrapper {
display: inline-flex !important;
align-items: center;
height: 35px;
font-size: 15px;
}
.o_calendar_view .o_calendar_widget {
.fc-timeGridDay-view .fc-axis,
.fc-timeGridWeek-view .fc-axis {
padding-left: 0px;
}
.o_calendar_widget {
.fc-dayGridMonth-view {
padding-left: 0px;
.fc-week-number {
display: none;
}
}
.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 {
.o_kanban_view .o_cp_pager .btn-group {
top: -1px;
}
.o_kanban_renderer {
@ -98,42 +51,35 @@ html .o_web_client .o_action_manager .o_action {
// 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;
margin-bottom: 10px !important;
}
&.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 {
@ -141,43 +87,17 @@ html .o_web_client .o_action_manager .o_action {
min-height: 35px;
}
}
.o_external_button {
margin-left: 10px;
margin-left: 5px;
}
.o_dropdown_button,
.o_datepicker_button {
top: 8px;
top: 50%;
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) {
display: block;
span:not(.o_field_translate) {
display: block;
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: initial;
&:active {
white-space: normal;
}
transform: translateY(-50%);
}
}
}
@ -185,20 +105,6 @@ html .o_web_client .o_action_manager .o_action {
@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;
@ -209,43 +115,13 @@ html .o_web_client .o_action_manager .o_action {
// 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;
top: 53px;
}
.o_nocontent_help {
box-shadow: none;
@ -256,112 +132,16 @@ html .o_web_client .o_action_manager .o_action {
}
}
// Sticky Header & Footer in List View
.o_list_view {
.table-responsive {
overflow: visible;
.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-mail-Form-chatter {
&.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;
@ -380,20 +160,56 @@ body:not(.o_statusbar_buttons) {
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;
}
// Color clue to tell the difference between a note and a public message
// 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
.o-mail-Chatter-top:has(.o-mail-Chatter-sendMessage.active) {
.o-mail-Composer {
background-color: lighten($o-brand-primary, 35%);
padding-top: 0.25rem !important;
}
@include media-breakpoint-up(sm) {
.o-mail-Composer {
padding-top: 0.5rem !important;
}
}
}
@include media-breakpoint-up(md) {
.app_settings_block > h2,
.app_settings_block > div > h2 {
@include o-position-sticky(0);
z-index: 10;
}
}
.o_list_table {
.o_handle_cell,
.o_list_record_remove {
vertical-align: middle;
}
}
.o_action_manager {
.dropdown-menu {
max-height: 70vh;
max-height: 70dvh;
}
.o_searchview_input {
padding-top: 0;
padding-bottom: 0;
}
.o_control_panel_main {
.btn {
white-space: nowrap;
}
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t
t-name="web_responsive.CustomFavoriteItem"
t-inherit="web.CustomFavoriteItem"
t-inherit-mode="extension"
>
<xpath expr="//AccordionItem/div[1]" position="attributes">
<attribute name="class" add="o_add_favorite_props" separator=" " />
</xpath>
<xpath
expr="//AccordionItem/div[1]/input[hasclass('o_input')]"
position="attributes"
>
<attribute name="class" add="o_add_favorite_name" separator=" " />
</xpath>
</t>
</templates>

View File

@ -5,37 +5,121 @@
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.html).
-->
<templates id="form_view" xml:space="preserve">
<templates id="form_view">
<!-- Template for buttons that display only the icon in xs -->
<t t-name="web_responsive.icon_button_create" owl="1">
<t t-name="web_responsive.icon_button_create">
<i t-attf-class="fa fa-plus" title="New" />
<span class="d-none d-sm-inline"> New</span>
<span class="d-none d-sm-inline ms-1">New</span>
</t>
<t t-name="web_responsive.icon_button_save" owl="1">
<t t-name="web_responsive.icon_button_save">
<i t-attf-class="fa fa-check" title="Save" />
<span class="d-none d-sm-inline"> Save</span>
<span class="d-none d-sm-inline ms-1">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-name="web_responsive.icon_button_discard">
<i t-attf-class="fa fa-undo" title="Discard" />
<span class="d-none d-sm-inline ms-1">Discard</span>
</t>
<t
t-name="web.ResponsiveFormView.Buttons"
t-name="web_responsive.FormView.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"
>
<xpath expr="//button[hasclass('o_form_button_cancel')]" position="attributes">
<attribute name="data-hotkey">d</attribute>
</xpath>
<xpath expr="//button[hasclass('o_form_button_save')]" position="replace">
<button
type="button"
class="btn btn-primary o_form_button_save"
data-hotkey="s"
t-on-click.stop="() => this.saveButtonClicked({closable: true})"
>
<t t-call="web_responsive.icon_button_save" />
</button>
</xpath>
<xpath expr="//button[hasclass('o_form_button_cancel')]" position="replace">
<button
type="button"
class="btn btn-secondary o_form_button_cancel"
data-hotkey="j"
t-on-click.stop="discard"
>
<t t-call="web_responsive.icon_button_discard" />
</button>
</xpath>
<xpath expr="//button[hasclass('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_responsive.SettingsFormView.Buttons"
t-inherit="web.SettingsFormView.Buttons"
t-inherit-mode="extension"
>
<xpath expr="//button[hasclass('o_form_button_save')]" position="replace">
<button
type="button"
class="btn btn-primary o_form_button_save"
data-hotkey="s"
t-on-click.stop="() => this.saveButtonClicked({closable: true})"
>
<t t-call="web_responsive.icon_button_save" />
</button>
</xpath>
<xpath expr="//button[hasclass('o_form_button_cancel')]" position="replace">
<button
type="button"
class="btn btn-secondary o_form_button_cancel"
data-hotkey="j"
t-on-click.stop="discard"
>
<t t-call="web_responsive.icon_button_discard" />
</button>
</xpath>
<xpath expr="//button[hasclass('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_responsive.FormView"
t-inherit="web.FormView"
t-inherit-mode="extension"
>
<xpath
expr="//button[contains(@class, 'o_form_button_create')]"
expr="//button[hasclass('o_form_button_create')][hasclass('btn-outline-primary')]"
position="replace"
>
<button
type="button"
class="btn btn-outline-primary o_form_button_create"
data-hotkey="c"
t-on-click.stop="create"
>
<t t-call="web_responsive.icon_button_create" />
</button>
</xpath>
<xpath
expr="//button[hasclass('o_form_button_create')][hasclass('btn-secondary')]"
position="replace"
>
<button
@ -49,57 +133,24 @@
</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"
>
<t t-name="web_responsive.FormStatusIndicator" t-inherit="web.FormStatusIndicator">
<!-- Change "Discard" button hotkey to "D" -->
<xpath
expr="//button[contains(@class, 'o_form_button_cancel')]"
position="attributes"
>
<xpath expr="//button[hasclass('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-name="web_responsive.KanbanView"
t-inherit="web.KanbanView"
t-inherit-mode="extension"
>
<!-- Add responsive icons to buttons -->
<xpath
expr="//button[contains(@class, 'o-kanban-button-new')]"
position="replace"
>
<xpath expr="//button[hasclass('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)"
t-on-click="() => this.createRecord()"
data-bounce-button=""
>
<t t-call="web_responsive.icon_button_create" />
@ -107,16 +158,12 @@
</xpath>
</t>
<t
t-name="web.ResponsiveListView.Buttons"
t-inherit="web.ListView.Buttons"
owl="1"
t-name="web_responsive.ListView"
t-inherit="web.ListView"
t-inherit-mode="extension"
>
<!-- Add responsive icons to buttons -->
<xpath
expr="//button[contains(@class, 'o_list_button_add')]"
position="replace"
>
<xpath expr="//button[hasclass('o_list_button_add')]" position="replace">
<button
type="button"
class="btn btn-primary o_list_button_add"
@ -127,10 +174,14 @@
<t t-call="web_responsive.icon_button_create" />
</button>
</xpath>
<xpath
expr="//button[contains(@class, 'o_list_button_save')]"
position="replace"
>
</t>
<t
t-name="web_responsive.ListView.Buttons"
t-inherit="web.ListView.Buttons"
t-inherit-mode="extension"
>
<!-- Add responsive icons to buttons -->
<xpath expr="//button[hasclass('o_list_button_save')]" position="replace">
<button
type="button"
class="btn btn-primary o_list_button_save"
@ -140,15 +191,12 @@
<t t-call="web_responsive.icon_button_save" />
</button>
</xpath>
<xpath
expr="//button[contains(@class, 'o_list_button_discard')]"
position="replace"
>
<xpath expr="//button[hasclass('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-click.stop="onClickDiscard"
t-on-mousedown="onMouseDownDiscard"
>
<t t-call="web_responsive.icon_button_discard" />

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Kirollos Risk
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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

@ -1,9 +1,10 @@
/* Copyright 2023 Tecnativa - Carlos Roca
* Copyright 2023 Taras Shabaranskyi
* 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 {
.o-autocomplete--dropdown-menu .ui-menu-item-wrapper {
white-space: initial;
}
}

View File

@ -0,0 +1,21 @@
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
.o_xxl_form_view {
.o_form_sheet_bg {
overflow: unset;
.o_form_sheet {
overflow: auto;
&::-webkit-scrollbar {
width: 10px;
}
&::-webkit-scrollbar-thumb {
background: $o-brand-odoo;
border-radius: 6px;
}
}
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates>
<t
t-name="web_responsive.StatusBarButtons"
t-inherit="web.StatusBarButtons"
t-inherit-mode="extension"
>
<xpath expr="//Dropdown" position="attributes">
<attribute name="hotkey">'shift+a'</attribute>
</xpath>
<xpath expr="//t[@t-set-slot='toggler']" position="replace">
<t t-set-slot="toggler">
<i class="fa fa-sliders me-1 me-sm-2" />
<span class="d-none d-sm-inline">Action</span>
</t>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,54 @@
/** @odoo-module **/
/* global QUnit */
/* eslint init-declarations: "warn" */
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {click, getFixture, mount, patchWithCleanup} from "@web/../tests/helpers/utils";
import {Component, xml} from "@odoo/owl";
import {makeTestEnv} from "@web/../tests/helpers/mock_env";
import {actionService} from "@web/webclient/actions/action_service";
import {browser} from "@web/core/browser/browser";
import {menuService} from "@web/webclient/menus/menu_service";
import {notificationService} from "@web/core/notifications/notification_service";
import {NavBar} from "@web/webclient/navbar/navbar";
import {registry} from "@web/core/registry";
import {hotkeyService} from "@web/core/hotkeys/hotkey_service";
import {uiService} from "@web/core/ui/ui_service";
const serviceRegistry = registry.category("services");
class MySystrayItem extends Component {}
MySystrayItem.template = xml`<li class="my-item">my item</li>`;
let baseConfig;
let target;
QUnit.module("AppsMenu Search", {
async beforeEach() {
target = getFixture();
serviceRegistry.add("menu", menuService);
serviceRegistry.add("action", actionService);
serviceRegistry.add("notification", notificationService);
serviceRegistry.add("hotkey", hotkeyService);
serviceRegistry.add("ui", uiService);
patchWithCleanup(browser, {
setTimeout: (handler, delay, ...args) => handler(...args),
clearTimeout: () => undefined,
});
const menus = {
root: {id: "root", children: [1, 2], name: "root", appID: "root"},
1: {id: 1, children: [], name: "App0", appID: 1, xmlid: "menu_1"},
2: {id: 2, children: [], name: "App1", appID: 2, xmlid: "menu_2"},
};
const serverData = {menus};
baseConfig = {serverData};
},
});
QUnit.test("can be rendered", async (assert) => {
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, {env});
await click(target, "button.o_grid_apps_menu__button");
assert.containsOnce(target, ".app-menu-container .search-input");
});

View File

@ -0,0 +1,87 @@
/** @odoo-module **/
/* global QUnit */
/* eslint init-declarations: "warn" */
/* Copyright 2023 Taras Shabaranskyi
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {
click,
getFixture,
mount,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import {Component, xml} from "@odoo/owl";
import {makeTestEnv} from "@web/../tests/helpers/mock_env";
import {actionService} from "@web/webclient/actions/action_service";
import {browser} from "@web/core/browser/browser";
import {menuService} from "@web/webclient/menus/menu_service";
import {notificationService} from "@web/core/notifications/notification_service";
import {NavBar} from "@web/webclient/navbar/navbar";
import {registry} from "@web/core/registry";
import {hotkeyService} from "@web/core/hotkeys/hotkey_service";
import {uiService} from "@web/core/ui/ui_service";
const serviceRegistry = registry.category("services");
class MySystrayItem extends Component {}
MySystrayItem.template = xml`<li class="my-item">my item</li>`;
let baseConfig;
let target;
QUnit.module("AppsMenu", {
async beforeEach() {
target = getFixture();
serviceRegistry.add("menu", menuService);
serviceRegistry.add("action", actionService);
serviceRegistry.add("notification", notificationService);
serviceRegistry.add("hotkey", hotkeyService);
serviceRegistry.add("ui", uiService);
patchWithCleanup(browser, {
setTimeout: (handler, delay, ...args) => handler(...args),
clearTimeout: () => undefined,
});
const menus = {
root: {id: "root", children: [1, 2], name: "root", appID: "root"},
1: {id: 1, children: [], name: "App0", appID: 1, xmlid: "menu_1"},
2: {id: 2, children: [], name: "App1", appID: 2, xmlid: "menu_2"},
};
const serverData = {menus};
baseConfig = {serverData};
},
});
QUnit.test("can be rendered", async (assert) => {
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, {env});
assert.containsOnce(
target,
".o_grid_apps_menu button.o_grid_apps_menu__button",
"1 apps menu button present"
);
});
QUnit.test("can be opened and closed", async (assert) => {
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, {env});
await click(target, "button.o_grid_apps_menu__button");
await nextTick();
assert.containsOnce(target, ".o-app-menu-list");
await click(target, "button.o_grid_apps_menu__button");
await nextTick();
assert.containsNone(target, ".o-app-menu-list");
});
QUnit.test("can be active", async (assert) => {
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, {env});
await click(target, "button.o_grid_apps_menu__button");
await nextTick();
env.services.menu.setCurrentMenu(1);
await nextTick();
assert.containsOnce(target, '.o-app-menu-item.active[data-menu-xmlid="menu_1"]');
env.services.menu.setCurrentMenu(2);
await nextTick();
assert.containsOnce(target, '.o-app-menu-item.active[data-menu-xmlid="menu_2"]');
});

View File

@ -0,0 +1,4 @@
# Copyright 2023 Taras Shabaranskyi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import test_ir_http

View File

@ -0,0 +1,35 @@
# Copyright 2023 Taras Shabaranskyi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import json
import re
from odoo.tests import HttpCase, tagged
@tagged("-at_install", "post_install")
class TestIrHttp(HttpCase):
def _test_session_info(self, session_info):
apps_menu = session_info.get("apps_menu")
self.assertIsNotNone(apps_menu)
self.assertTrue("search_type" in apps_menu)
self.assertTrue("theme" in apps_menu)
def _find_session_info(self, line_items):
key = "odoo.__session_info__ = "
line = next(filter(lambda item: key in item, line_items), None)
self.assertIsNotNone(line)
match = re.match(rf".*{key}(.*);", line)
self.assertIsNotNone(match)
return match.group(1)
def test_session_info(self):
self.authenticate("admin", "admin")
r = self.url_open("/web")
self.assertEqual(r.status_code, 200)
self.assertIsInstance(r.text, str)
line_items = r.text.splitlines()
self.assertTrue(bool(line_items))
session_info_str = self._find_session_info(line_items)
self.assertIsInstance(session_info_str, str)
self._test_session_info(json.loads(session_info_str))

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Taras Shabaranskyi
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="res_users_view_form_apps_menu_preferences" model="ir.ui.view">
<field name="name">res.users.apps.menu.preferences.form</field>
<field name="model">res.users</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field
name="apps_menu_search_type"
string="Search Type"
help="Apps Menu Search Type"
/>
</group>
<group>
<field
name="apps_menu_theme"
string="Theme"
help="Apps Menu Theme"
/>
</group>
</group>
<div class="mt-3">
<h3>Search Type Help</h3>
<table class="table table-bordered w-100 w-lg-50">
<tr>
<th>Canonical</th>
<td>uses a standard algorithm</td>
</tr>
<tr>
<th>Fuse</th>
<td>a new search algorithm is used</td>
</tr>
<tr>
<th>Command Palette</th>
<td>the standard odoo search tool</td>
</tr>
</table>
</div>
</form>
</field>
</record>
<record
id="res_users_view_form_apps_menu_preferences_action"
model="ir.actions.act_window"
>
<field name="name">Apps Menu Preferences</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.users</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field
name="view_ids"
eval="[
Command.clear(),
Command.create({'view_mode': 'form', 'view_id': ref('web_responsive.res_users_view_form_apps_menu_preferences')})
]"
/>
</record>
</odoo>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2018 Alexandre Díaz
Copyright 2021 ITerra - Sergey Shebanin
Copyright 2023 Onestein - Anjeel Haria
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<template
id="responsive_web_layout"
inherit_id="web.layout"
name="Responsive Layout"
>
<xpath expr="//meta[last()]" position="after">
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
</xpath>
</template>
</odoo>