3
0
Fork 0

[MIG] web_responsive: Migration to 15.0

15.0-ocabot-merge-pr-2789-by-pedrobaeza-bump-patch
Sergey Shebanin 2021-10-21 17:40:06 +03:00
parent 6ebff91b59
commit 62722a7a2f
42 changed files with 2236 additions and 2415 deletions

View File

@ -0,0 +1 @@
../../../../web_responsive

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -14,16 +14,16 @@ 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/14.0/web_responsive
:target: https://github.com/OCA/web/tree/15.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-14-0/web-14-0-web_responsive
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/162/14.0
:target: https://runbot.odoo-community.org/runbot/162/15.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
|badge1| |badge2| |badge3| |badge4| |badge5|
This module adds responsiveness to web backend.
@ -152,7 +152,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 smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_responsive%0Aversion:%2014.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:%2015.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.
@ -196,11 +196,14 @@ promote its widespread use.
.. |maintainer-Tardo| image:: https://github.com/Tardo.png?size=40px
:target: https://github.com/Tardo
:alt: Tardo
.. |maintainer-SplashS| image:: https://github.com/SplashS.png?size=40px
:target: https://github.com/SplashS
:alt: SplashS
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-Yajo| |maintainer-Tardo|
|maintainer-Yajo| |maintainer-Tardo| |maintainer-SplashS|
This module is part of the `OCA/web <https://github.com/OCA/web/tree/14.0/web_responsive>`_ project on GitHub.
This module is part of the `OCA/web <https://github.com/OCA/web/tree/15.0/web_responsive>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -1,30 +1,49 @@
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2017-2018 Tecnativa - Jairo Llopis
# Copyright 2018-2019 Tecnativa - Alexandre Díaz
# Copyright 2021 ITerra - Sergey Shebanin
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
"name": "Web Responsive",
"summary": "Responsive web client, community-supported",
"version": "14.0.1.0.2",
"version": "15.0.1.0.0",
"category": "Website",
"website": "https://github.com/OCA/web",
"author": "LasLabs, Tecnativa, " "Odoo Community Association (OCA)",
"author": "LasLabs, Tecnativa, ITerra, " "Odoo Community Association (OCA)",
"license": "LGPL-3",
"installable": True,
"depends": ["web", "mail"],
"development_status": "Production/Stable",
"maintainers": ["Yajo", "Tardo"],
"data": ["views/assets.xml", "views/res_users.xml", "views/web.xml"],
"qweb": [
"static/src/xml/apps.xml",
"static/src/xml/form_buttons.xml",
"static/src/xml/menu.xml",
"static/src/xml/navbar.xml",
"static/src/xml/attachment_viewer.xml",
"static/src/xml/discuss.xml",
"static/src/xml/control_panel.xml",
"static/src/xml/search_panel.xml",
],
"maintainers": ["Yajo", "Tardo", "SplashS"],
"data": ["views/res_users.xml", "views/web.xml"],
"assets": {
"web.assets_backend": [
"/web_responsive/static/src/legacy/css/web_responsive.scss",
"/web_responsive/static/src/legacy/js/web_responsive.js",
"/web_responsive/static/src/legacy/css/kanban_view_mobile.scss",
"/web_responsive/static/src/legacy/js/kanban_renderer_mobile.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/navbar/main_navbar.scss",
"/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/attachment_viewer/attachment_viewer.scss",
"/web_responsive/static/src/components/attachment_viewer/attachment_viewer.esm.js",
"/web_responsive/static/src/components/hotkey/hotkey.scss",
],
"web.assets_qweb": [
"/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/navbar/main_navbar.xml",
"/web_responsive/static/src/components/search_panel/search_panel.xml",
"/web_responsive/static/src/components/attachment_viewer/attachment_viewer.xml",
"/web_responsive/static/src/components/hotkey/hotkey.xml",
],
},
"sequence": 1,
}

View File

@ -1,179 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * web_responsive
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2021-09-23 07:34+0000\n"
"Last-Translator: Saeed Raeisi <saeed.raesi2020@gmail.com>\n"
"Language-Team: none\n"
"Language: fa\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.3.2\n"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/search_panel.xml:0
#, python-format
msgid "All"
msgstr "همه"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#, python-format
msgid "CLEAR"
msgstr "پاک کردن"
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users__chatter_position
msgid "Chatter Position"
msgstr "موقعیت چت باکس"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Create"
msgstr "ایجاد"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Discard"
msgstr "لغو"
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users__display_name
msgid "Display Name"
msgstr "نام نمایشی"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Edit"
msgstr "ویرایش"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#: code:addons/web_responsive/static/src/xml/search_panel.xml:0
#, python-format
msgid "FILTER"
msgstr "فیلتر"
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users__id
msgid "ID"
msgstr "شناسه"
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users____last_update
msgid "Last Modified on"
msgstr "آخرین ویرایش در"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/attachment_viewer.xml:0
#: code:addons/web_responsive/static/src/xml/attachment_viewer.xml:0
#, python-format
msgid "Maximize"
msgstr "بزرگ کردن"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/attachment_viewer.xml:0
#: code:addons/web_responsive/static/src/xml/attachment_viewer.xml:0
#, python-format
msgid "Minimize"
msgstr "کوچک کردن"
#. module: web_responsive
#: model:ir.model.fields.selection,name:web_responsive.selection__res_users__chatter_position__normal
msgid "Normal"
msgstr "عادی"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Quick actions"
msgstr "اقدامات سریع"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#: code:addons/web_responsive/static/src/xml/search_panel.xml:0
#, python-format
msgid "SEE RESULT"
msgstr "مشاهده نتیجه"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Save"
msgstr "ذخیره"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/apps.xml:0
#, python-format
msgid "Search menus..."
msgstr "جستجو در منو..."
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#, python-format
msgid "Search..."
msgstr "جستجو..."
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/menu.xml:0
#, python-format
msgid "Shift"
msgstr "شیفت"
#. module: web_responsive
#: model:ir.model.fields.selection,name:web_responsive.selection__res_users__chatter_position__sided
msgid "Sided"
msgstr "طرف"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Today"
msgstr "امروز"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/js/kanban_renderer_mobile.js:0
#, python-format
msgid "Undefined"
msgstr "تعریف نشده"
#. module: web_responsive
#: model:ir.model,name:web_responsive.model_res_users
msgid "Users"
msgstr "کاربران"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#, python-format
msgid "View switcher"
msgstr "تعویض کننده نما"

View File

@ -1,177 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * web_responsive
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fa_IR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/search_panel.xml:0
#, python-format
msgid "All"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#, python-format
msgid "CLEAR"
msgstr ""
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users__chatter_position
msgid "Chatter Position"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Create"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Discard"
msgstr ""
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users__display_name
msgid "Display Name"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Edit"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#: code:addons/web_responsive/static/src/xml/search_panel.xml:0
#, python-format
msgid "FILTER"
msgstr ""
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users__id
msgid "ID"
msgstr ""
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users____last_update
msgid "Last Modified on"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/attachment_viewer.xml:0
#: code:addons/web_responsive/static/src/xml/attachment_viewer.xml:0
#, python-format
msgid "Maximize"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/attachment_viewer.xml:0
#: code:addons/web_responsive/static/src/xml/attachment_viewer.xml:0
#, python-format
msgid "Minimize"
msgstr ""
#. module: web_responsive
#: model:ir.model.fields.selection,name:web_responsive.selection__res_users__chatter_position__normal
msgid "Normal"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Quick actions"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#: code:addons/web_responsive/static/src/xml/search_panel.xml:0
#, python-format
msgid "SEE RESULT"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Save"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/apps.xml:0
#, python-format
msgid "Search menus..."
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#, python-format
msgid "Search..."
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/menu.xml:0
#, python-format
msgid "Shift"
msgstr ""
#. module: web_responsive
#: model:ir.model.fields.selection,name:web_responsive.selection__res_users__chatter_position__sided
msgid "Sided"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_buttons.xml:0
#, python-format
msgid "Today"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/js/kanban_renderer_mobile.js:0
#, python-format
msgid "Undefined"
msgstr ""
#. module: web_responsive
#: model:ir.model,name:web_responsive.model_res_users
msgid "Users"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/control_panel.xml:0
#, python-format
msgid "View switcher"
msgstr ""

View File

@ -6,15 +6,15 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2021-09-25 08:34+0000\n"
"Last-Translator: Rémi <remi@le-filament.com>\n"
"PO-Revision-Date: 2020-07-22 12:19+0000\n"
"Last-Translator: c2cdidier <didier.donze@camptocamp.com>\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.3.2\n"
"X-Generator: Weblate 3.10\n"
#. module: web_responsive
#. openerp-web
@ -54,7 +54,7 @@ msgstr "Annuler"
#: code:addons/web_responsive/static/src/xml/form_view.xml:0
#, python-format
msgid "Edit"
msgstr "Modifier"
msgstr "Editer"
#. module: web_responsive
#. openerp-web
@ -99,9 +99,9 @@ msgstr "Rechercher dans les menus..."
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/menu.xml:0
#, fuzzy, python-format
#, python-format
msgid "Shift"
msgstr "Shift"
msgstr ""
#. module: web_responsive
#: model:ir.model.fields.selection,name:web_responsive.selection__res_users__chatter_position__sided
@ -120,19 +120,18 @@ msgstr "Utilisateurs"
msgid ""
"btn btn-secondary o_mail_discuss_button_multi_user_channel d-md-block d-none"
msgstr ""
"btn btn-secondary o_mail_discuss_button_multi_user_channel d-md-block d-none"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/apps.xml:0
#: code:addons/web_responsive/static/src/xml/document_viewer.xml:0
#, fuzzy, python-format
#, python-format
msgid "false"
msgstr "false"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/document_viewer.xml:0
#, python-format
msgid "modal o_modal_fullscreen o_document_viewer o_responsive_document_viewer"
msgstr "modal o_modal_fullscreen o_document_viewer o_responsive_document_viewer"
msgstr ""

View File

@ -9,19 +9,18 @@ class ResUsers(models.Model):
chatter_position = fields.Selection(
[("normal", "Normal"), ("sided", "Sided")],
string="Chatter Position",
default="sided",
)
def __init__(self, pool, cr):
"""Override of __init__ to add access rights.
Access rights are disabled by default, but allowed on some specific
fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS.
"""
super().__init__(pool, cr)
# duplicate list to avoid modifying the original reference
type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS)
type(self).SELF_WRITEABLE_FIELDS.extend(["chatter_position"])
# duplicate list to avoid modifying the original reference
type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS)
type(self).SELF_READABLE_FIELDS.extend(["chatter_position"])
"""Override to add access rights.
Access rights are disabled by default, but allowed on some specific
fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS.
"""
@property
def SELF_READABLE_FIELDS(self):
return super().SELF_READABLE_FIELDS + ["chatter_position"]
@property
def SELF_WRITEABLE_FIELDS(self):
return super().SELF_WRITEABLE_FIELDS + ["chatter_position"]

View File

@ -1,28 +1,40 @@
This module adds responsiveness to web backend.
Features for all devices:
**Features for all devices**:
* New navigation with an app drawer
* New navigation with the fullscreen app menu
.. image:: https://user-images.githubusercontent.com/973709/48417193-09a1e080-e74a-11e8-8a0c-e73eb689b2fb.gif
.. image:: ../static/img/appmenu.gif
* Quick menu search from the app drawer
* Quick menu search inside the app menu
.. image:: https://user-images.githubusercontent.com/973709/48417213-17576600-e74a-11e8-846a-57691e82636b.gif
Features for mobile:
* 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/big_checkboxes.gif
**Features for mobile**:
* App-specific submenus are shown on full screen when toggling them from the
"hamburger" menu
.. image:: https://user-images.githubusercontent.com/973709/48417297-51286c80-e74a-11e8-9a47-22c810b18c43.gif
* View type picker dropdown displays confortably
* View type picker dropdown displays comfortably
.. image:: https://user-images.githubusercontent.com/973709/50964322-e3d55580-14c6-11e9-8249-48db9539600f.gif
* Top app bar is always visible, but the control panel is hidden when
scrolling down, to save some vaulable vertical space
scrolling down, to save some valuable vertical space
.. image:: https://user-images.githubusercontent.com/973709/50964496-5cd4ad00-14c7-11e9-9261-fd223a329d02.gif
@ -35,20 +47,38 @@ Features for mobile:
.. image:: https://user-images.githubusercontent.com/973709/50965168-1d0ec500-14c9-11e9-82a0-dfee82ed0861.gif
* Search panel is hidden on small screens.
* Search panel is collapsed to mobile version on small screens.
.. image:: ../static/img/search_panel.gif
Features for computers:
* Followers and send button is displayed on mobile. Avatar is hidden.
* Keyboard shortcuts for easier navigation, **using ``Alt + Shift + [key]``**
combination instead of just ``Alt + [key]``.
See https://github.com/odoo/odoo/issues/30068 to understand why.
.. image:: ../static/img/chatter.gif
* Followers and send button is displayed on mobile. Avatar is hidden.
.. image:: ../static/img/chatter.gif
* Scrollable dropdowns
.. image:: ../static/img/dropdown_scroll.gif
* Interface is adapted dynamically on device rotation
.. image:: ../static/img/rotate.gif
**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:: https://user-images.githubusercontent.com/973709/48417578-ff341680-e74a-11e8-8881-017709e912bc.png
* Autofocus on search menu box when opening the drawer
* Autofocus on search menu box when opening the app menu
.. image:: https://user-images.githubusercontent.com/973709/48417213-17576600-e74a-11e8-846a-57691e82636b.gif
@ -64,32 +94,8 @@ Features for computers:
.. image:: ../static/img/chatter_topbar.gif
* AppMenu waits for action finished to show the view
.. image:: ../static/img/appmenu.gif
* Sticky header & footer in list view
.. image:: ../static/img/listview.gif
* Sticky statusbar in form view
.. image:: ../static/img/formview.gif
* Followers and send button is displayed on mobile. Avatar is hidden.
.. image:: ../static/img/chatter.gif
* When the chatter is configured 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
* Bigger checkboxes in list view
.. image:: ../static/img/big_checkboxes.gif
* Scrollable dropdowns
.. image:: ../static/img/dropdown_scroll.gif

View File

@ -1,7 +1,2 @@
* To view the full experience in a device, the page must be loaded with the
device screen size. This means that, if you change the size of your browser,
you should reload the web client to get the full experience for that
new size. This is Odoo's own limitation.
* App navigation with keyboard.
* Handle long titles on forms in a better way
* Standard sticky headers seems to not work properly on iOS Safari/Chrome (see #1626).

View File

@ -0,0 +1,208 @@
/** @odoo-module **/
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
import {Dropdown} from "@web/core/dropdown/dropdown";
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";
const {Component} = owl;
const {useState, useRef} = owl.hooks;
/**
* @extends Dropdown
*/
export class AppsMenu extends Dropdown {
setup() {
super.setup();
useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", () => this.close());
useBus(this.env.bus, "APPS_MENU:CLOSE", () => this.close());
}
}
/**
* 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) {
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,
});
useAutofocus({selector: "input"});
this.searchBarInput = useRef("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();
}
willPatch() {
// 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;
}
}
patched() {
// Scroll to selected element on keyboard navigation
if (this.state.results.length) {
const listElement = this.el.querySelector(".search-results");
const activeElement = this.el.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.results =
query === ""
? []
: 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() {
const repeatable = {
allowRepeat: true,
};
useHotkey(
"ArrowDown",
() => {
this.state.offset++;
},
repeatable
);
useHotkey(
"ArrowUp",
() => {
this.state.offset--;
},
repeatable
);
useHotkey(
"Tab",
() => {
this.state.offset++;
},
repeatable
);
useHotkey(
"Shift+Tab",
() => {
this.state.offset--;
},
repeatable
);
useHotkey("Home", () => {
this.state.offset = 0;
});
useHotkey("End", () => {
this.state.offset = this.state.results.length - 1;
});
useHotkey("Enter", () => {
if (this.state.results.length) {
this.el.querySelector(".highlight").click();
}
});
}
_onKeyDown(ev) {
if (ev.code === "Escape") {
ev.stopPropagation();
ev.preventDefault();
const query = this.searchBarInput.el.value;
if (query) {
this.searchBarInput.el.value = "";
} else {
this.env.bus.trigger("APPS_MENU:CLOSE");
}
}
}
}
AppsMenuSearchBar.template = "web_responsive.AppsMenuSearchResults";
Object.assign(NavBar.components, {AppsMenu, AppsMenuSearchBar});

View File

@ -0,0 +1,188 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
@mixin full-screen-dropdown {
border: none;
box-shadow: none;
min-height: calc(100vh - #{$o-navbar-height});
min-height: calc(var(--vh100, 100vh) - #{$o-navbar-height});
position: fixed;
margin: 0;
width: 100vw;
z-index: 200;
left: 0 !important;
}
// Iconized full screen apps menu
.o_navbar_apps_menu {
.fade-enter-active,
.fade-leave-active {
transition: opacity 100ms ease;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.dropdown-menu {
@include full-screen-dropdown();
cursor: pointer;
background: url("../../img/home-menu-bg-overlay.svg"),
linear-gradient(
to bottom,
$o-brand-odoo,
desaturate(lighten($o-brand-odoo, 20%), 15)
);
background-size: cover;
border-radius: 0;
// Display apps in a grid
align-content: flex-start;
display: flex !important;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
@include media-breakpoint-up(lg) {
padding: {
left: calc((100vw - 850px) / 2);
right: calc((100vw - 850px) / 2);
}
}
.dropdown-item {
padding: 0;
}
.o_app {
background: none;
img {
box-shadow: none;
margin-bottom: 5px;
transition: 300ms ease;
transition-property: box-shadow, transform;
}
a {
outline: 0;
height: 100%;
display: flex;
align-items: center;
text-align: center;
flex-direction: column;
justify-content: flex-start;
white-space: normal;
color: gray("white") !important;
padding: 15px 0 10px;
font-size: 1.25rem;
text-shadow: 1px 1px 1px rgba(gray("black"), 0.4);
border-radius: 4px;
transition: 300ms ease;
transition-property: background-color;
background: none;
&:focus {
background-color: rgba(gray("white"), 0.05);
}
}
&:hover img,
a:focus img {
transform: translateY(-3px);
box-shadow: 0 9px 12px -4px rgba(gray("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: middle;
box-shadow: inset 0 1px 0 rgba(gray("white"), 0.1),
0 1px 0 rgba(gray("black"), 0.1);
text-shadow: 0 1px 0 rgba(gray("black"), 0.5);
border-radius: 4px;
padding: 0.4rem 0.8rem;
margin-bottom: 1rem;
background-color: rgba(gray("white"), 0.1);
@include media-breakpoint-up(md) {
padding: 0.8rem 1.2rem;
}
.search-icon {
color: gray("white");
font-size: 1.5rem;
margin-right: 1rem;
padding-top: 1px;
}
.form-control {
height: 2rem;
background: none;
border: none;
color: gray("white");
display: block;
padding: 1px 2px 2px 2px;
box-shadow: none;
&::placeholder {
color: gray("white");
opacity: 0.5;
}
}
}
// Allow to scroll only on results, keeping static search box above
.search-results {
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: gray("white");
cursor: pointer;
line-height: 2.5rem;
padding-left: 3.5rem;
white-space: normal;
font-weight: 100;
&.highlight,
&:hover {
background-color: rgba(gray("black"), 0.11);
}
b {
font-weight: 700;
}
}
}
}
}

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2021 ITerra - Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t t-inherit="web.NavBar.AppsMenu" t-inherit-mode="extension" owl="1">
<xpath expr="//Dropdown" position="replace">
<!-- Same hotkey as in EE -->
<AppsMenu
hotkey="'a'"
title="'Home Menu'"
manualOnly="true"
class="o_navbar_apps_menu"
>
<t t-set-slot="toggler">
<i class="fa fa-th-large" />
</t>
<t t-transition="o_notification_fade">
<AppsMenuSearchBar />
<MenuItem
t-foreach="apps"
t-as="app"
t-key="app.id"
class="o_app"
t-att-class="{ o_dropdown_active: menuService.getCurrentApp() === app }"
payload="app"
>
<a t-att-href="getMenuItemHref(app)">
<img
class="o-app-icon"
draggable="false"
t-attf-src="data:image/png;base64,{{app.webIconData}}"
/>
<div t-esc="app.name" />
</a>
</MenuItem>
</t>
</AppsMenu>
</xpath>
</t>
<!-- Search bar -->
<t t-name="web_responsive.AppsMenuSearchResults" owl="1">
<div
class="search-container"
t-att-class="state.results.length ? '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"
/>
</div>
<div t-if="state.results.length" class="search-results">
<t t-foreach="state.results" t-as="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('data:image/png;base64,&quot; + menu.webIconData + &quot;')&quot; : ''"
t-attf-href="#menu_id={{menu.id}}&amp;action_id={{menu.actionID}}"
t-att-data-menu-id="menu.id"
t-att-data-action-id="menu.actionID"
draggable="false"
t-esc="result"
/>
</t>
</div>
</div>
</t>
<t t-inherit="web.Dropdown" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('dropdown-menu')]" position="attributes">
<attribute name="t-transition">fade</attribute>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,22 @@
/** @odoo-module **/
/* Copyright 2021 ITerra - Sergey Shebanin
* 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";
const {useState} = owl.hooks;
// Patch attachment viewer to add min/max buttons capability
patch(AttachmentViewer.prototype, "web_responsive.AttachmentViewer", {
setup() {
this._super();
this.state = useState({
maximized: false,
});
},
// Disable auto-close to allow to use form in edit mode.
isCloseable() {
return false;
},
});

View File

@ -0,0 +1,56 @@
/* 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_chatter_position_sided .o_DialogManager_dialog {
/* Show sided viewer on large screens */
@include media-breakpoint-up(lg) {
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%;
}
/* 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%;
}
}
}
@include media-breakpoint-down(md) {
.o_AttachmentViewer_headerItemButtonMinimize,
.o_AttachmentViewer_headerItemButtonMaximize {
display: none;
}
}
}
/* Attachment Viewer Max/Min buttons only are useful in sided mode */
.o_web_client:not(.o_chatter_position_sided) {
.o_AttachmentViewer_headerItemButtonMinimize,
.o_AttachmentViewer_headerItemButtonMaximize {
display: none;
}
}

View File

@ -3,14 +3,6 @@
Copyright 2021 Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t t-inherit="mail.Dialog" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_Dialog')]" position="attributes">
<attribute
name="t-attf-class"
t-translation="off"
>o_Dialog_{{dialog.record['constructor'].name}}</attribute>
</xpath>
</t>
<t t-inherit="mail.AttachmentViewer" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_AttachmentViewer')]" position="attributes">
<attribute
@ -19,8 +11,8 @@
>state.maximized ? 'o_AttachmentViewer_maximized' : ''</attribute>
</xpath>
<xpath
expr="//div[hasclass('o_AttachmentViewer_header')]/div[hasclass('o-autogrow')]"
position="after"
expr="//div[hasclass('o_AttachmentViewer_headerItemButtonClose')]"
position="before"
>
<div
t-if="!state.maximized"

View File

@ -0,0 +1,45 @@
/** @odoo-module **/
/* Copyright 2021 ITerra - Sergey Shebanin
* 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 {SearchBar} from "@web/search/search_bar/search_bar";
import {deviceContext} from "@web_responsive/components/ui_context.esm";
import {patch} from "web.utils";
const {useState, useContext} = owl.hooks;
// In v15.0 there are two ControlPanel's. They are mostly the same and are used in legacy and new owl views.
// We extend them two mostly the same way.
// Patch legacy control panel to add states for mobile quick search
patch(LegacyControlPanel.prototype, "web_responsive.LegacyControlPanelMobile", {
setup() {
this._super();
this.state = useState({
mobileSearchMode: "",
});
this.ui = useContext(deviceContext);
},
setMobileSearchMode(ev) {
this.state.mobileSearchMode = ev.detail;
},
});
// Patch control panel to add states for mobile quick search
patch(ControlPanel.prototype, "web_responsive.ControlPanelMobile", {
setup() {
this._super();
this.state = useState({
mobileSearchMode: "",
});
this.ui = useContext(deviceContext);
},
setMobileSearchMode(ev) {
this.state.mobileSearchMode = ev.detail;
},
});
patch(SearchBar, "web_responsive.SearchBarMobile", {
template: "web_responsive.SearchBar",
});

View File

@ -0,0 +1,304 @@
/* 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: 30px;
}
// 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.
position: sticky;
left: 0;
z-index: 3;
// Arrange buttons to use space better
.o_cp_top_left,
.o_cp_top_right {
flex: 1 1 100%;
}
.o_cp_top_left {
flex-basis: 80%;
max-width: 80%;
}
.o_cp_top_right {
flex-basis: 20%;
}
.o_cp_bottom {
position: relative; // Necessary for dropdown menu positioning
display: block;
margin: 0;
}
.o_cp_bottom_left {
float: left;
margin: 5px 0;
}
.o_cp_bottom_right {
float: right;
height: 30px;
padding-left: 10px;
margin: 5px 0;
}
.o_cp_bottom_right,
.o_cp_pager {
white-space: nowrap;
}
.o_cp_pager {
margin-bottom: 0;
}
.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%;
padding-right: 0.5rem !important;
}
.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;
}
}
.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 {
width: auto;
@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 {
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-primary;
}
.o_searchview_facet {
display: inline-flex;
order: 1;
}
.o_searchview_autocomplete {
top: 3rem;
}
}
.o_mobile_search_filter {
padding-bottom: 15%;
> .dropdown {
flex-direction: column;
line-height: 2rem;
width: 100%;
margin: 15px 5px 0px 5px;
border: solid 1px darken(gray("200"), 20%);
}
.dropdown.show > .dropdown-toggle {
background-color: gray("200");
}
.dropdown-toggle {
width: 100%;
text-align: left;
&:after {
top: auto;
}
}
.dropdown-item:before {
top: auto;
}
.dropdown-item.focus {
background-color: white;
}
.dropdown-menu {
// Here we use !important because of popper js adding custom style
// to element so to override it use !important
position: relative !important;
top: 0 !important;
left: 0 !important;
width: 100%;
max-height: 100%;
box-shadow: none;
border: none;
color: gray("600");
.divider {
margin: 0px;
}
> li > a {
padding: 10px 26px;
}
}
}
.o_mobile_search_show_result {
padding: 10px;
font-size: 15px;
}
}

View File

@ -0,0 +1,296 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2021 Sergey Shebanin
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 and state.mobileSearchMode == 'quick'">
<button
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="ui.isSmall and state.mobileSearchMode == 'full'"
t-call="web_responsive.LegacyMobileSearchView"
/>
<t t-if="ui.isSmall and state.mobileSearchMode == ''">
<button
class="btn btn-link fa fa-search"
t-on-click.stop="state.mobileSearchMode = 'quick'"
/>
</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">
<span
class="o_mobile_search_close float-left mt16 mb16 mr8 ml16"
t-on-click.stop="state.mobileSearchMode = '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="model.dispatch('clearQuery')"
>
<t>CLEAR</t>
</span>
</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.stop="state.mobileSearchMode = ''"
>
<t>SEE RESULT</t>
</div>
</div>
</t>
<!-- Wowl control panel templates -->
<t t-inherit="web.ControlPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//nav[hasclass('o_cp_switch_buttons')]" position="replace">
<t t-if="ui.size lt= ui.SIZES.LG">
<t
t-set="view"
t-value="env.config.viewSwitcherEntries.find((v) => v.active)"
/>
<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_{{view.type}} {{view.icon}} active"
/>
</t>
<t
t-foreach="env.config.viewSwitcherEntries"
t-as="view"
t-key="view.type"
>
<button
class="btn btn-light fa o_switch_view"
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
t-att-data-tooltip="view.name"
t-on-click="onViewClicked(view.type)"
/>
</t>
</Dropdown>
</t>
<t t-else="">
<nav class="btn-group o_cp_switch_buttons">
<t
t-foreach="env.config.viewSwitcherEntries"
t-as="view"
t-key="view.type"
>
<button
class="btn btn-light fa fa-lg o_switch_view "
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
t-att-data-tooltip="view.name"
t-on-click="onViewClicked(view.type)"
/>
</t>
</nav>
</t>
</xpath>
<xpath expr="//SearchBar" position="replace">
<!-- This duplication is hack because owl has a bug https://github.com/odoo/owl/issues/949 -->
<SearchBar
t-if="state.mobileSearchMode == 'quick'"
mobileSearchMode="state.mobileSearchMode"
searchMenus="searchMenus"
t-on-set-mobile-view.stop="setMobileSearchMode"
/>
<SearchBar
t-else=""
mobileSearchMode="state.mobileSearchMode"
searchMenus="searchMenus"
t-on-set-mobile-view.stop="setMobileSearchMode"
/>
</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.SearchBar" owl="1">
<div>
<t t-if="!env.isSmall" t-call="web.SearchBar" />
<t t-if="env.isSmall and props.mobileSearchMode == 'quick'">
<div class="o_searchview o_searchview_quick">
<button
class="btn btn-link fa fa-arrow-left"
t-on-click.stop="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="trigger('set-mobile-view', 'full')"
/>
</div>
</t>
<t
t-if="env.isSmall and props.mobileSearchMode == 'full'"
t-call="web_responsive.MobileSearchView"
/>
<t t-if="env.isSmall and props.mobileSearchMode == ''">
<div
class="o_searchview o_searchview_mobile"
role="search"
aria-autocomplete="list"
t-on-click.stop="trigger('set-mobile-view', 'quick')"
>
<button class="btn btn-link fa fa-search" />
</div>
</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="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="trigger('set-mobile-view', '')"
>
<t>SEE RESULT</t>
</div>
</div>
</div>
</t>
</templates>

View File

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

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2021 ITerra - Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates id="template" xml:space="preserve">
<t t-inherit="web.NavBar.SectionsMenu" t-inherit-mode="extension" owl="1">
<xpath
expr="//t[@t-foreach='sections']//t[@t-set='hotkey']"
position="attributes"
>
<attribute
name="t-value"
t-translation="off"
>'shift+' + ((section_index + 1) % 10).toString()</attribute>
</xpath>
<xpath
expr="//t[@t-if='currentAppSectionsExtra.length']//t[@t-set='hotkey']"
position="attributes"
>
<attribute
name="t-value"
t-translation="off"
>'shift+' + (sectionsVisibleCount + 1 % 10).toString()</attribute>
</xpath>
</t>
<t t-inherit="web.Pager" t-inherit-mode="extension" owl="1">
<xpath expr="//button[hasclass('o_pager_previous')]" position="attributes">
<attribute
name="t-att-accesskey"
>props.withAccessKey ? 'z' : false</attribute>
</xpath>
<xpath expr="//button[hasclass('o_pager_next')]" position="attributes">
<attribute
name="t-att-accesskey"
>props.withAccessKey ? 'x' : false</attribute>
</xpath>
</t>
<t t-inherit="web.UserMenu.shortcutsTable" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('row')]" position="attributes">
<attribute name="class" separator=" " add="justify-content-center" />
</xpath>
<xpath expr="//div[hasclass('row')]/div" position="attributes">
<attribute name="class" />
</xpath>
<xpath expr="//span[text()='a']" position="replace">
<span class="o_key">e</span>
</xpath>
<xpath expr="//span[text()='a']" position="replace">
<span class="o_key">e</span>
</xpath>
<xpath expr="//span[text()='j']" position="replace">
<span class="o_key">d</span>
</xpath>
<xpath expr="//span[text()='j']" position="replace">
<span class="o_key">d</span>
</xpath>
<xpath expr="//span[text()='p']" position="replace">
<span class="o_key">z</span>
</xpath>
<xpath expr="//span[text()='p']" position="replace">
<span class="o_key">z</span>
</xpath>
<xpath expr="//span[text()='n']" position="replace">
<span class="o_key">x</span>
</xpath>
<xpath expr="//span[text()='n']" position="replace">
<span class="o_key">x</span>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,53 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
// Main navbar (with systray items: user menu, debug menu...)
@include media-breakpoint-down(sm) {
.o_main_navbar {
// Hide big things
.o_menu_brand,
.oe_topbar_name {
display: none;
}
// Collapse sections menu to hamburger
.o_menu_sections {
width: 46px;
}
.o_menu_sections_more {
.dropdown-toggle {
font-size: 17px;
}
.fa-plus:before {
content: "\f0c9";
}
}
// User menu paddings
.o_usr_menu {
margin: 0;
}
// Custom fullscreen layout when showing submenus
.dropdown-menu {
@include full-screen-dropdown();
background-color: $dropdown-bg;
overflow: auto;
// Higher height for dropdown items, for those with sausage fingers
.dropdown-item {
padding: {
bottom: 0.5rem;
top: 1rem;
}
font-size: 16px;
a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2021 ITerra - Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t t-inherit="web.NavBar" t-inherit-mode="extension" owl="1">
<xpath expr="//t[@t-call='web.NavBar.SectionsMenu']" position="attributes">
<attribute
name="t-if"
t-translation="off"
>currentAppSections.length</attribute>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,57 @@
/** @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";
const {useContext} = owl.hooks;
// Patch search panel to add functionality for mobile view
patch(SearchPanel.prototype, "web_responsive.SearchPanelMobile", {
setup() {
this._super();
this.state.mobileSearch = false;
this.ui = useContext(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,3 +1,6 @@
/* 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;

View File

@ -2,10 +2,10 @@
<!-- Copyright 2021 Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t t-inherit="web.SearchPanel" t-inherit-mode="extension" owl="1">
<t t-inherit="web.Legacy.SearchPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_search_panel')]" position="inside">
<div
t-if="device.isMobile"
t-if="ui.isSmall"
class="o_search_panel_summary"
t-on-click.stop="state.mobileSearch = true"
>
@ -27,11 +27,11 @@
</div>
<div
class="o_search_panel_content"
t-att-class="device.isMobile ? (state.mobileSearch ? 'o_mobile_search' : 'd-none'): ''"
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="device.isMobile" class="o_mobile_search_header">
<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"
@ -42,7 +42,7 @@
</div>
<xpath expr="//section" position="move" />
<div
t-if="device.isMobile"
t-if="ui.isSmall"
class="btn btn-primary o_mobile_search_show_result fixed-bottom"
t-on-click.stop="state.mobileSearch = false"
>

View File

@ -0,0 +1,46 @@
/** @odoo-module **/
/* Copyright 2021 ITerra - Sergey Shebanin
* 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";
const {Context} = owl;
// 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,
});
// 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.state;
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

@ -1,933 +0,0 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
$chatter_zone_width: 35%;
@mixin full-screen-dropdown {
border: none;
box-shadow: none;
display: flex;
flex-direction: column;
height: calc(100vh - #{$o-navbar-height});
position: fixed;
margin: 0;
width: 100vw;
z-index: 100;
// Inline style will override our `top`, so we need !important here
top: $o-navbar-height !important;
transform: none !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);
}
}
// Main navbar (with apps menu, user menu, debug menu...)
@include media-breakpoint-down(sm) {
.o_main_navbar {
// This allows to have a sane layout for mobiles
display: flex;
// Remove clutter to only have small icons that fit in a small screen
> .dropdown {
display: flex;
.navbar-toggler {
color: gray("white");
}
.o_menu_sections,
.o_menu_systray {
padding: 0;
}
}
// Whitespace before systray icons
.o_menu_systray {
margin-left: auto;
}
// Hide big things
.o_menu_brand,
.o_menu_sections,
.oe_topbar_name {
display: none;
}
// Fix toggler button padding
.o-menu-toggle {
cursor: pointer;
padding: 0 $o-horizontal-padding;
}
// Custom fullscreen layout when showing submenus
.o_menu_sections.show {
@include full-screen-dropdown();
background-color: $dropdown-bg;
overflow: auto;
.show {
display: flex;
flex-direction: column;
.dropdown-menu {
margin-left: 1rem;
min-width: auto;
width: calc(100vw - 2rem);
}
}
> li,
.o_menu_entry_lvl_1,
.o_menu_header_lvl_1 {
// Homogeneous background color
background-color: $dropdown-bg;
color: $dropdown-link-color;
&:hover {
background-color: $dropdown-link-hover-bg;
color: $dropdown-link-hover-color;
}
// Disable the .o-no-caret class effect, to display the caret
&.o-no-caret::after {
content: "";
}
// Fix a strange glitch leaving headers invisible
.dropdown-header {
color: $dropdown-header-color;
}
}
}
// Custom fullscreen layout for systray items
.o_mail_systray_dropdown.show {
@include full-screen-dropdown();
// Fix stretchy images
.o_mail_preview_image {
align-items: center;
display: flex;
flex-direction: row;
img {
display: block;
height: auto;
}
}
}
// Higher height for dropdown items, for those with sausage fingers
.dropdown-menu .dropdown-item {
padding: {
bottom: 0.5rem;
top: 0.5rem;
}
}
}
}
.o_main_navbar {
color: color-yiq($o-brand-odoo);
> ul > li > a,
> ul > li > label {
color: color-yiq($o-brand-odoo);
}
.dropdown-menu.show {
max-height: calc(100vh - #{$o-navbar-height});
}
}
// Iconized full screen apps menu
.o_menu_apps {
user-select: none;
a.full {
width: $o-navbar-height;
text-align: center;
}
.dropdown-menu.show {
opacity: 1;
visibility: visible;
}
.dropdown-menu {
@include full-screen-dropdown();
opacity: 0;
visibility: hidden;
transition: visibility 100ms ease, opacity 100ms ease;
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;
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 {
align-items: center;
display: flex;
padding: 10px 0;
border-radius: 4px;
flex-direction: column;
justify-content: flex-start;
background: none;
transition: 300ms ease;
transition-property: background-color;
white-space: normal;
text-align: center;
img {
box-shadow: none;
transition: 300ms ease;
transition-property: box-shadow, transform;
}
&:focus {
background-color: rgba(gray("white"), 0.05);
}
// Size depends on screen
width: 33.33333333%;
@include media-breakpoint-up(sm) {
width: 25%;
}
@include media-breakpoint-up(md) {
width: 16.6666666%;
}
}
.o_app:hover img {
transform: translateY(-3px);
box-shadow: 0 9px 12px -4px rgba(gray("black"), 0.3);
}
// Hide app icons when searching
.has-results ~ .o_app {
display: none;
}
.o-app-icon {
height: auto;
max-width: 6rem;
}
.o-app-name {
color: gray("white");
margin-top: 4px;
font-size: 1.25rem;
text-shadow: 1px 1px 1px rgba(gray("black"), 0.4);
}
// Search input for menus
.form-row {
width: 100%;
}
.o-menu-search-result {
align-items: center;
background-position: left;
background-repeat: no-repeat;
background-size: contain;
cursor: pointer;
padding-left: 3rem;
white-space: normal;
}
.search-container {
padding-top: 1rem;
padding-bottom: 1.5rem;
.search-input {
margin-bottom: 0 !important;
padding: 0;
.input-group {
box-shadow: inset 0 1px 0 rgba(gray("white"), 0.1),
0 1px 0 rgba(gray("black"), 0.1);
text-shadow: 0 1px 0 rgba(gray("black"), 0.5);
border-radius: 4px;
padding: 0.4rem 0.8rem;
background-color: rgba(gray("white"), 0.1);
@include media-breakpoint-up(md) {
padding: 0.8rem 1.2rem;
}
.input-group-prepend {
span.fa {
color: gray("white");
font-size: 1.5rem;
margin-right: 1rem;
padding-top: 1px;
}
}
.form-control {
height: 2rem;
background: none;
border: none;
color: gray("white");
display: block;
padding: 1px 2px 2px 2px;
box-shadow: none;
&::placeholder {
color: gray("white");
opacity: 0.5;
}
}
}
}
}
// Allow to scroll only on results, keeping static search box above
.search-container.has-results {
height: 100%;
.search-results {
max-height: calc(100vh - 47px - 6em);
overflow-y: hidden;
overflow-x: scroll;
overflow: auto;
background: url("../img/home-menu-bg-overlay.svg"),
linear-gradient(to bottom, gray("200"), gray("white"));
background-position: center;
background-size: cover;
}
}
}
}
// 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;
}
}
max-width: 100%;
}
// 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: 30px;
}
}
// Mobile Control panel (breadcrumbs, search box, buttons...)
@include media-breakpoint-down(sm) {
.o_control_panel {
// 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.
position: sticky;
left: 0;
z-index: 3;
// Arrange buttons to use space better
.o_cp_top_left,
.o_cp_top_right {
flex: 1 1 100%;
}
.o_cp_top_left {
flex-basis: 80%;
}
.o_cp_top_right {
flex-basis: 20%;
}
.o_cp_bottom {
position: relative; // Necessary for dropdown menu positioning
display: block;
margin: 0;
}
.o_cp_bottom_left {
float: left;
margin: 5px 0;
}
.o_cp_bottom_right {
float: right;
height: 30px;
padding-left: 10px;
margin: 5px 0;
}
.o_cp_bottom_right,
.o_cp_pager {
white-space: nowrap;
}
.o_cp_pager {
margin-bottom: 0;
}
.o_cp_bottom_left > .o_cp_action_menus {
padding-right: 0;
.o_dropdown_title,
.fa-chevron-right,
.fa-chevron-down {
display: none;
}
.o_dropdown_toggler_btn {
margin: 0px 2px;
}
@include media-breakpoint-down(xs) {
.o_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) {
padding-left: 0;
}
&::before {
content: none;
padding-right: 0;
}
&:nth-last-of-type(1n + 3) {
display: none;
}
&:nth-last-of-type(2) {
&::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 {
padding: 1px 0px 3px 0px;
&.o_searchview_mobile {
cursor: pointer;
}
&.o_searchview_quick {
display: flex;
flex: 1 1 auto;
align-items: center;
.o_searchview_input_container {
flex: 1 1 auto;
}
}
}
}
.o_calendar_view .o_calendar_widget {
.fc-timeGridDay-view .fc-axis,
.fc-timeGridWeek-view .fc-axis {
padding-left: 0px;
}
.fc-dayGridMonth-view {
padding-left: 0px;
.fc-week-number {
display: none;
}
}
.fc-dayGridYear-view {
padding-left: 0px;
> .fc-month-container > .fc-month {
width: 100%;
}
}
.fc-timeGridDay-view .fc-widget-header {
margin: 0 4px;
}
.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;
}
}
}
}
// Normal views
.o_content,
.modal-content {
max-width: 100%;
// Form views
.o_form_view {
.o_form_sheet {
max-width: calc(100% - 32px);
overflow-x: auto;
}
.o_FormRenderer_chatterContainer {
padding-top: 0;
.o_Activity_info {
flex-wrap: wrap;
}
.o_ActivityBox_title {
margin-bottom: 0;
}
.o_MessageList_separatorDate {
padding-bottom: 0;
}
}
// Sided chatter scrolling behavior
.o_Chatter {
height: fit-content;
.o_Chatter_fixedPanel {
position: sticky;
top: 0;
z-index: 1;
background-color: white;
padding-bottom: 10px;
}
.o_Chatter_scrollPanel {
overflow: initial;
}
}
// Sticky statusbar
.o_form_statusbar {
position: sticky;
top: 0;
z-index: 2;
}
// Support for long title (with ellipsis)
.oe_title {
span.o_field_widget:not(.oe_inline) {
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: initial;
&:active {
white-space: normal;
}
}
}
@include media-breakpoint-down(sm) {
min-width: auto;
// More buttons border
.oe_button_box {
.o_dropdown_more {
button:last-child {
border-right: 1px solid gray("400");
}
}
}
// Avoid overflow on forms with title and/or button box
.oe_title {
max-width: 100%;
}
.oe_button_box + .oe_title,
.oe_button_box + .oe_avatar + .oe_title {
width: 100%;
}
// Avoid overflow on modals
.o_form_sheet {
min-width: auto;
}
// Render website inputs properly in phones
.o_group .o_field_widget.o_text_overflow {
// Overrides another !important
width: auto !important;
}
// Make all input groups vertical
.o_group_col_6 {
width: 100%;
}
// Statusbar buttons dropdown for mobiles
.o_statusbar_buttons_dropdown {
border: {
bottom: 0;
radius: 0;
top: 0;
}
height: 100%;
}
.o_statusbar_buttons.dropdown-menu > .btn {
border-radius: 0;
border: 0;
width: 100%;
margin-bottom: 0.2rem;
&:last-child {
margin-bottom: 0;
}
}
.o_statusbar_status {
// Arrow from rightmost button exceeds allowed width
.o_arrow_button:first-child::before {
content: none;
display: none;
}
}
// Full width in form sheets
.o_form_sheet,
.o_FormRenderer_chatterContainer {
min-width: auto;
max-width: 98%;
}
// Settings pages
.app_settings_block {
.row {
margin: 0;
}
}
.o_FormRenderer_chatterContainer {
padding-top: initial;
// Display send button on small screens
.o_thread_composer {
padding-left: $o-mail-thread-avatar-size * 0.5;
.o_composer_button_send {
display: initial !important; // Forced in core
}
.o_chatter_avatar {
display: none;
}
}
.o_chatter_topbar {
> .o_topbar_right_area {
flex: 1 0 auto;
flex-wrap: wrap;
max-width: 100%;
// Display followers on small screens
.o_followers {
display: initial !important; // Forced in core
@include media-breakpoint-down(xs) {
padding-bottom: 50px;
}
}
}
}
}
}
}
//No content message improvements on mobile
@include media-breakpoint-down(md) {
.o_view_nocontent {
top: 80px;
}
.o_nocontent_help {
box-shadow: none;
}
.o_sample_data_disabled {
display: none;
}
}
// Sided chatter, if user wants
.o_chatter_position_sided & {
@include media-breakpoint-up(lg) {
.o_form_view:not(.o_form_nosheet) {
display: flex;
flex-flow: row nowrap;
height: 100%;
.o_form_sheet_bg {
flex: 1 1 auto;
overflow: auto;
> .o_form_sheet {
min-width: unset;
}
}
.o_FormRenderer_chatterContainer {
border-left: 1px solid gray("400");
flex: 0 0 $chatter_zone_width;
max-width: initial;
min-width: initial;
overflow: auto;
.o_chatter_header_container {
padding-top: $grid-gutter-width * 0.5;
top: 0;
position: sticky;
background-color: $o-view-background-color;
z-index: 1;
// Scrollable input text to avoid hide conversation & buttons
.o_composer_text_field {
max-height: 120px;
overflow-y: auto !important; /* Forced because Odoo uses inline style */
}
.o_attachments_list {
overflow: auto;
max-height: $o-mail-attachment-image-size * 3;
margin-top: 0.4em;
}
.o_attachments_previews {
overflow: auto;
max-height: $o-mail-attachment-image-size * 6;
}
}
}
}
}
}
}
// Sticky Header & Footer in List View
.o_list_view {
.table-responsive {
.o_list_table {
// th & td are here for compatibility with chrome
thead tr:nth-child(1) th {
position: sticky;
top: 0;
z-index: 1;
}
thead tr:nth-child(1) th {
background-color: $o-list-footer-bg-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;
}
}
}
}
// Big checkboxes
.o_list_view {
.custom-checkbox:not(.o_boolean_toggle) {
margin-right: 10px;
.custom-control-label {
top: -6px;
&::after {
width: 24px;
height: 24px;
}
&::before {
outline: none !important;
border: 1px solid #4c4c4c;
width: 24px;
height: 24px;
}
}
}
}
// Waiting Cursor
.oe_wait {
cursor: progress;
}
// Attachment Viewer
.o_web_client.o_chatter_position_sided .o_Dialog_AttachmentViewer {
/* Show sided viewer on large screens */
@include media-breakpoint-up(lg) {
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);
.o_AttachmentViewer_name {
display: contents;
}
width: $chatter_zone_width;
&.o_AttachmentViewer_maximized {
width: 100%;
}
/* 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%;
}
}
}
@include media-breakpoint-down(md) {
.o_AttachmentViewer_headerItemButtonMinimize,
.o_AttachmentViewer_headerItemButtonMaximize {
display: none;
}
}
}
/* Attachment Viewer Max/Min buttons only are useful in sided mode */
.o_web_client:not(.o_chatter_position_sided) {
.o_AttachmentViewer_headerItemButtonMinimize,
.o_AttachmentViewer_headerItemButtonMaximize {
display: none;
}
}
.o_control_panel {
// Filter Menu
// Cut long filters names in the filters menu
.o_filter_menu {
.o_menu_item {
width: auto;
@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.show {
.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;
}
}
}
}
// Shortcut table ui improvement
.o_shortcut_table {
width: 100%;
}

View File

@ -1,629 +0,0 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2018 Tecnativa - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define("web_responsive", function (require) {
"use strict";
const ActionManager = require("web.ActionManager");
const AbstractWebClient = require("web.AbstractWebClient");
const AppsMenu = require("web.AppsMenu");
const BasicController = require("web.BasicController");
const config = require("web.config");
const core = require("web.core");
const FormRenderer = require("web.FormRenderer");
const Menu = require("web.Menu");
const RelationalFields = require("web.relational_fields");
const ListRenderer = require("web.ListRenderer");
const CalendarRenderer = require("web.CalendarRenderer");
const patchMixin = require("web.patchMixin");
const AttachmentViewer = require("mail/static/src/components/attachment_viewer/attachment_viewer.js");
const PatchableAttachmentViewer = patchMixin(AttachmentViewer);
const ControlPanel = require("web.ControlPanel");
const SearchPanel = require("web/static/src/js/views/search_panel.js");
/* global owl */
const {QWeb, Context} = owl;
const {useState, useContext} = owl.hooks;
/* Hide AppDrawer in desktop and mobile modes.
* To avoid delays in pages with a lot of DOM nodes we make
* sub-groups' with 'querySelector' to improve the performance.
*/
function closeAppDrawer() {
_.defer(function () {
// Need close AppDrawer?
var menu_apps_dropdown = document.querySelector(".o_menu_apps .dropdown");
$(menu_apps_dropdown)
.has(".dropdown-menu.show")
.find("> a")
.dropdown("toggle");
// Need close Sections Menu?
// TODO: Change to 'hide' in modern Bootstrap >4.1
var menu_sections = document.querySelector(
".o_menu_sections li.show .dropdown-toggle"
);
$(menu_sections).dropdown("toggle");
// Need close Mobile?
var menu_sections_mobile = document.querySelector(".o_menu_sections.show");
$(menu_sections_mobile).collapse("hide");
});
}
/**
* Reduce menu data to a searchable format understandable by fuzzy.js
*
* `AppsMenu.init()` gets `menuData` in a format similar to this (only
* relevant data is shown):
*
* ```js
* {
* [...],
* children: [
* // This is a menu entry:
* {
* action: "ir.actions.client,94", // Or `false`
* children: [... similar to above "children" key],
* name: "Actions",
* parent_id: [146, "Settings/Technical/Actions"], // Or `false`
* },
* ...
* ]
* }
* ```
*
* 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.action) {
var key = menu.parent_id ? menu.parent_id[1] + "/" : "";
memo[key + menu.name] = menu;
}
if (menu.children.length) {
_.reduce(menu.children, findNames, memo);
}
return memo;
}
AppsMenu.include({
events: _.extend(
{
"keydown .search-input input": "_searchResultsNavigate",
"input .search-input input": "_searchMenusSchedule",
"click .o-menu-search-result": "_searchResultChosen",
"shown.bs.dropdown": "_searchFocus",
"hidden.bs.dropdown": "_searchReset",
"hide.bs.dropdown": "_hideAppsMenu",
},
AppsMenu.prototype.events
),
/**
* Rescue some menu data stripped out in original method.
*
* @override
*/
init: function (parent, menuData) {
this._super.apply(this, arguments);
// Keep base64 icon for main menus
for (const n in this._apps) {
this._apps[n].web_icon_data = menuData.children[n].web_icon_data;
}
// Store menu data in a format searchable by fuzzy.js
this._searchableMenus = _.reduce(menuData.children, findNames, {});
// Search only after timeout, for fast typers
this._search_def = false;
},
/**
* @override
*/
start: function () {
this.$search_container = this.$(".search-container");
this.$search_input = this.$(".search-input input");
this.$search_results = this.$(".search-results");
return this._super.apply(this, arguments);
},
/**
* Prevent the menu from being opened twice
*
* @override
*/
_onAppsMenuItemClicked: function (ev) {
this._super.apply(this, arguments);
ev.preventDefault();
ev.stopPropagation();
},
/**
* Get all info for a given menu.
*
* @param {String} key
* Full path to requested menu.
*
* @returns {Object}
* Menu definition, plus extra needed keys.
*/
_menuInfo: function (key) {
const original = this._searchableMenus[key];
return _.extend(
{
action_id: parseInt(original.action.split(",")[1], 10),
},
original
);
},
/**
* Autofocus on search field on big screens.
*/
_searchFocus: function () {
if (!config.device.isMobile) {
// This timeout is necessary since the menu has a 100ms fading animation
setTimeout(() => this.$search_input.focus(), 100);
}
},
/**
* Reset search input and results
*/
_searchReset: function () {
this.$search_container.removeClass("has-results");
this.$search_results.empty();
this.$search_input.val("");
},
/**
* Schedule a search on current menu items.
*/
_searchMenusSchedule: function () {
this._search_def = new Promise((resolve) => {
setTimeout(resolve, 50);
});
this._search_def.then(this._searchMenus.bind(this));
},
/**
* Search among available menu items, and render that search.
*/
_searchMenus: function () {
const query = this.$search_input.val();
if (query === "") {
this.$search_container.removeClass("has-results");
this.$search_results.empty();
return;
}
var results = fuzzy.filter(query, _.keys(this._searchableMenus), {
pre: "<b>",
post: "</b>",
});
this.$search_container.toggleClass("has-results", Boolean(results.length));
this.$search_results.html(
core.qweb.render("web_responsive.MenuSearchResults", {
results: results,
widget: this,
})
);
},
/**
* Use chooses a search result, so we navigate to that menu
*
* @param {jQuery.Event} event
*/
_searchResultChosen: function (event) {
event.preventDefault();
event.stopPropagation();
const $result = $(event.currentTarget),
text = $result.text().trim(),
data = $result.data(),
suffix = ~text.indexOf("/") ? "/" : "";
// Load the menu view
this.trigger_up("menu_clicked", {
action_id: data.actionId,
id: data.menuId,
previous_menu_id: data.parentId,
});
// Find app that owns the chosen menu
const app = _.find(this._apps, function (_app) {
return text.indexOf(_app.name + suffix) === 0;
});
// Update navbar menus
core.bus.trigger("change_menu_section", app.menuID);
},
/**
* Navigate among search results
*
* @param {jQuery.Event} event
*/
_searchResultsNavigate: function (event) {
// Find current results and active element (1st by default)
const all = this.$search_results.find(".o-menu-search-result"),
pre_focused = all.filter(".active") || $(all[0]);
let offset = all.index(pre_focused),
key = event.key;
// Keyboard navigation only supports search results
if (!all.length) {
return;
}
// Transform tab presses in arrow presses
if (key === "Tab") {
event.preventDefault();
key = event.shiftKey ? "ArrowUp" : "ArrowDown";
}
switch (key) {
// Pressing enter is the same as clicking on the active element
case "Enter":
pre_focused.click();
break;
// Navigate up or down
case "ArrowUp":
offset--;
break;
case "ArrowDown":
offset++;
break;
default:
// Other keys are useless in this event
return;
}
// Allow looping on results
if (offset < 0) {
offset = all.length + offset;
} else if (offset >= all.length) {
offset -= all.length;
}
// Switch active element
const new_focused = $(all[offset]);
pre_focused.removeClass("active");
new_focused.addClass("active");
this.$search_results.scrollTo(new_focused, {
offset: {
top: this.$search_results.height() * -0.5,
},
});
},
/*
* Control if AppDrawer can be closed
*/
_hideAppsMenu: function () {
return !this.$("input").is(":focus");
},
});
BasicController.include({
/**
* Close the AppDrawer if the data set is dirty and a discard dialog
* is opened
*
* @override
*/
canBeDiscarded: function (recordID) {
if (this.model.isDirty(recordID || this.handle)) {
closeAppDrawer();
}
return this._super.apply(this, arguments);
},
});
Menu.include({
events: _.extend(
{
// Clicking a hamburger menu item should close the hamburger
"click .o_menu_sections [role=menuitem]": "_onClickMenuItem",
// Opening any dropdown in the navbar should hide the hamburger
"show.bs.dropdown .o_menu_systray, .o_menu_apps": "_hideMobileSubmenus",
},
Menu.prototype.events
),
start: function () {
this.$menu_toggle = this.$(".o-menu-toggle");
return this._super.apply(this, arguments);
},
/**
* Hide menus for current app if you're in mobile
*/
_hideMobileSubmenus: function () {
if (
config.device.isMobile &&
this.$menu_toggle.is(":visible") &&
this.$section_placeholder.is(":visible")
) {
this.$section_placeholder.collapse("hide");
}
},
/**
* Prevent hide the menu (should be closed when action is loaded)
*
* @param {ClickEvent} ev
*/
_onClickMenuItem: function (ev) {
ev.stopPropagation();
},
/**
* No menu brand in mobiles
*
* @override
*/
_updateMenuBrand: function () {
if (!config.device.isMobile) {
return this._super.apply(this, arguments);
}
},
});
RelationalFields.FieldStatus.include({
/**
* Fold all on mobiles.
*
* @override
*/
_setState: function () {
this._super.apply(this, arguments);
if (config.device.isMobile) {
_.map(this.status_information, (value) => {
value.fold = true;
});
}
},
});
// Sticky Column Selector
ListRenderer.include({
_renderView: function () {
const self = this;
return this._super.apply(this, arguments).then(() => {
const $col_selector = self.$el.find(
".o_optional_columns_dropdown_toggle"
);
if ($col_selector.length !== 0) {
const $th = self.$el.find("thead>tr:first>th:last");
$col_selector.appendTo($th);
}
});
},
_onToggleOptionalColumnDropdown: function (ev) {
// FIXME: For some strange reason the 'stopPropagation' call
// in the main method don't work. Invoking here the same method
// does the expected behavior... O_O!
// This prevents the action of sorting the column from being
// launched.
ev.stopPropagation();
this._super.apply(this, arguments);
},
});
// Responsive view "action" buttons
FormRenderer.include({
/**
* In mobiles, put all statusbar buttons in a dropdown.
*
* @override
*/
_renderHeaderButtons: function () {
const $buttons = this._super.apply(this, arguments);
if (
!config.device.isMobile ||
$buttons.children("button:not(.o_invisible_modifier)").length <= 2
) {
return $buttons;
}
// $buttons must be appended by JS because all events are bound
const $dropdown = $(
core.qweb.render("web_responsive.MenuStatusbarButtons")
);
$buttons.addClass("dropdown-menu").appendTo($dropdown);
return $dropdown;
},
});
CalendarRenderer.include({
_getFullCalendarOptions: function () {
var options = this._super.apply(this, arguments);
if (config.device.isMobile) {
options.views.dayGridMonth.columnHeaderFormat = "ddd";
}
return options;
},
});
// Hide AppDrawer or Menu when the action has been completed
ActionManager.include({
/**
* @override
*/
_appendController: function () {
this._super.apply(this, arguments);
closeAppDrawer();
},
});
/**
* Use ALT+SHIFT instead of ALT as hotkey triggerer.
*
* HACK https://github.com/odoo/odoo/issues/30068 - See it to know why.
*
* Cannot patch in `KeyboardNavigationMixin` directly because it's a mixin,
* not a `Class`, and altering a mixin's `prototype` doesn't alter it where
* it has already been used.
*
* Instead, we provide an additional mixin to be used wherever you need to
* enable this behavior.
*/
var KeyboardNavigationShiftAltMixin = {
/**
* Alter the key event to require pressing Shift.
*
* This will produce a mocked event object where it will seem that
* `Alt` is not pressed if `Shift` is not pressed.
*
* The reason for this is that original upstream code, found in
* `KeyboardNavigationMixin` is very hardcoded against the `Alt` key,
* so it is more maintainable to mock its input than to rewrite it
* completely.
*
* @param {keyEvent} keyEvent
* Original event object
*
* @returns {keyEvent}
* Altered event object
*/
_shiftPressed: function (keyEvent) {
const alt = keyEvent.altKey || keyEvent.key === "Alt",
newEvent = _.extend({}, keyEvent),
shift = keyEvent.shiftKey || keyEvent.key === "Shift";
// Mock event to make it seem like Alt is not pressed
if (alt && !shift) {
newEvent.altKey = false;
if (newEvent.key === "Alt") {
newEvent.key = "Shift";
}
}
return newEvent;
},
_onKeyDown: function (keyDownEvent) {
return this._super(this._shiftPressed(keyDownEvent));
},
_onKeyUp: function (keyUpEvent) {
return this._super(this._shiftPressed(keyUpEvent));
},
};
// Include the SHIFT+ALT mixin wherever
// `KeyboardNavigationMixin` is used upstream
AbstractWebClient.include(KeyboardNavigationShiftAltMixin);
// TODO: use default odoo device context when it will be realized
const deviceContext = new Context({
isMobile: config.device.isMobile,
size_class: config.device.size_class,
SIZES: config.device.SIZES,
});
window.addEventListener(
"resize",
owl.utils.debounce(() => {
const state = deviceContext.state;
if (state.isMobile !== config.device.isMobile) {
state.isMobile = !state.isMobile;
}
if (state.size_class !== config.device.size_class) {
state.size_class = config.device.size_class;
}
}, 15)
);
// Patch attachment viewer to add min/max buttons capability
PatchableAttachmentViewer.patch("web_responsive.AttachmentViewer", (T) => {
class AttachmentViewerPatchResponsive extends T {
constructor() {
super(...arguments);
this.state = useState({
maximized: false,
});
}
// Disable auto-close to allow to use form in edit mode.
isCloseable() {
return false;
}
}
return AttachmentViewerPatchResponsive;
});
QWeb.components.AttachmentViewer = PatchableAttachmentViewer;
// Patch control panel to add states for mobile quick search
ControlPanel.patch("web_responsive.ControlPanelMobile", (T) => {
class ControlPanelPatchResponsive extends T {
constructor() {
super(...arguments);
this.state = useState({
mobileSearchMode: "",
});
this.device = useContext(deviceContext);
}
}
return ControlPanelPatchResponsive;
});
// Patch search panel to add functionality for mobile view
SearchPanel.patch("web_responsive.SearchPanelMobile", (T) => {
class SearchPanelPatchResponsive extends T {
constructor() {
super(...arguments);
this.state.mobileSearch = false;
this.device = useContext(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;
}
}
return SearchPanelPatchResponsive;
});
return {
deviceContext: deviceContext,
};
});

View File

@ -1,3 +1,6 @@
/* Copyright 2019 Odoo S.A.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
@include media-breakpoint-down(sm) {
.o_kanban_view.o_kanban_grouped {
display: block;
@ -30,6 +33,11 @@
width: 100%;
height: $o-kanban-mobile-tabs-height;
overflow-x: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
.o_kanban_mobile_tab {
height: $o-kanban-mobile-tabs-height;
@ -40,6 +48,8 @@
&.o_current {
font-weight: bold;
border-bottom: 3px solid $o-brand-primary;
background-color: gray("600");
font-weight: bold;
}
.o_column_title {
@ -86,6 +96,9 @@
}
}
}
.modal {
z-index: 1052;
}
.o_kanban_view .o_column_quick_create {
.o_quick_create_folded {
display: none !important;
@ -95,3 +108,9 @@
}
}
}
.o_kanban_view.o_kanban_ungrouped {
.o_kanban_record {
padding-left: 16px !important;
padding-right: 16px !important;
}
}

View File

@ -0,0 +1,357 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
$chatter_zone_width: 35%;
// 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);
}
}
// 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;
}
}
max-width: 100%;
}
@include media-breakpoint-down(sm) {
.o_calendar_view .o_calendar_widget {
.fc-timeGridDay-view .fc-axis,
.fc-timeGridWeek-view .fc-axis {
padding-left: 0px;
}
.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;
}
}
}
}
// Normal views
.o_content,
.modal-content {
max-width: 100%;
// Form views
.o_form_view {
.o_form_sheet {
max-width: calc(100% - 32px);
overflow-x: auto;
}
.o_FormRenderer_chatterContainer {
padding-top: 0;
.o_Activity_info {
flex-wrap: wrap;
}
.o_ActivityBox_title {
margin-bottom: 0;
}
.o_MessageList_separatorDate {
padding-bottom: 0;
}
}
// Sided chatter scrolling behavior
.o_Chatter {
height: fit-content;
.o_Chatter_fixedPanel {
position: sticky;
top: 0;
z-index: 1;
background-color: white;
padding-bottom: 10px;
}
.o_Chatter_scrollPanel {
overflow: initial;
}
}
// Sticky statusbar
.o_form_statusbar {
position: sticky;
top: 0;
z-index: 2;
}
// Support for long title (with ellipsis)
.oe_title {
span.o_field_widget:not(.oe_inline) {
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: initial;
&:active {
white-space: normal;
}
}
}
@include media-breakpoint-down(sm) {
min-width: auto;
// More buttons border
.oe_button_box {
.o_dropdown_more {
button:last-child {
border-right: 1px solid gray("400");
}
}
}
// Avoid overflow on forms with title and/or button box
.oe_title {
max-width: 100%;
}
.oe_button_box + .oe_title,
.oe_button_box + .oe_avatar + .oe_title {
width: 100%;
}
// Avoid overflow on modals
.o_form_sheet {
min-width: auto;
}
// Render website inputs properly in phones
.o_group .o_field_widget.o_text_overflow {
// Overrides another !important
width: auto !important;
}
// Make all input groups vertical
.o_group_col_6,
.o_group_col_8 {
width: 100%;
}
// Statusbar buttons dropdown for mobiles
.o_statusbar_buttons_dropdown {
border: {
bottom: 0;
radius: 0;
top: 0;
}
height: 100%;
}
.o_statusbar_buttons.dropdown-menu {
.btn {
border-radius: 0;
border: 0;
width: 100%;
margin-bottom: 0.2rem;
white-space: nowrap;
@include media-breakpoint-down(xs) {
max-width: 80vw;
overflow: hidden;
text-overflow: ellipsis;
}
&:last-child {
margin-bottom: 0;
}
}
}
.o_statusbar_status {
// Arrow from rightmost button exceeds allowed width
.o_arrow_button:first-child::before {
content: none;
display: none;
}
}
// Full width in form sheets
.o_form_sheet,
.o_FormRenderer_chatterContainer {
min-width: auto;
max-width: 98%;
}
// 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;
}
.o_Composer_sidebarMain {
display: none;
}
}
}
}
}
//No content message improvements on mobile
@include media-breakpoint-down(md) {
.o_view_nocontent {
top: 80px;
}
.o_nocontent_help {
box-shadow: none;
}
.o_sample_data_disabled {
display: none;
}
}
// Sided chatter, if user wants
.o_chatter_position_sided & {
@include media-breakpoint-up(lg) {
.o_form_view:not(.o_form_nosheet) {
display: flex;
flex-flow: row nowrap;
height: 100%;
.o_form_sheet_bg {
flex: 1 1 auto;
overflow: auto;
> .o_form_sheet {
min-width: unset;
}
}
.o_FormRenderer_chatterContainer {
border-left: 1px solid gray("400");
flex: 0 0 $chatter_zone_width;
max-width: initial;
min-width: initial;
overflow: auto;
.o_chatter_header_container {
padding-top: $grid-gutter-width * 0.5;
top: 0;
position: sticky;
background-color: $o-view-background-color;
z-index: 1;
// Scrollable input text to avoid hide conversation & buttons
.o_composer_text_field {
max-height: 120px;
overflow-y: auto !important; /* Forced because Odoo uses inline style */
}
.o_attachments_list {
overflow: auto;
max-height: $o-mail-attachment-image-size * 3;
margin-top: 0.4em;
}
.o_attachments_previews {
overflow: auto;
max-height: $o-mail-attachment-image-size * 6;
}
}
}
}
}
}
}
// Sticky Header & Footer in List View
.o_list_view {
.table-responsive {
.o_list_table {
// th & td are here for compatibility with chrome
thead tr:nth-child(1) th {
position: sticky;
top: 0;
z-index: 1;
}
thead tr:nth-child(1) th {
background-color: $o-list-footer-bg-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;
}
}
}
}
// Big checkboxes
.o_list_view {
.custom-checkbox:not(.o_boolean_toggle) {
margin-right: 10px;
.custom-control-label {
top: -6px;
&::after {
width: 24px;
height: 24px;
}
&::before {
outline: none !important;
border: 1px solid #4c4c4c;
width: 24px;
height: 24px;
}
}
}
}

View File

@ -1,4 +1,7 @@
odoo.define("web_responsive.KanbanRendererMobile", function (require) {
/* Copyright 2019 Odoo S.A.
* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define("web_responsive.KanbanRendererMobile", function (require) {
"use strict";
/**
@ -8,23 +11,19 @@
* Moreover, records in columns are lazy-loaded.
*/
var config = require("web.config");
var core = require("web.core");
var KanbanRenderer = require("web.KanbanRenderer");
var KanbanView = require("web.KanbanView");
var KanbanQuickCreate = require("web.kanban_column_quick_create");
const config = require("web.config");
const core = require("web.core");
const KanbanRenderer = require("web.KanbanRenderer");
const KanbanView = require("web.KanbanView");
const KanbanQuickCreate = require("web.kanban_column_quick_create");
var _t = core._t;
var qweb = core.qweb;
if (!config.device.isMobile) {
return;
}
const _t = core._t;
const qweb = core.qweb;
KanbanQuickCreate.include({
init() {
this._super.apply(this, arguments);
this.isMobile = true;
this.isMobile = config.device.isMobile;
},
/**
* KanbanRenderer will decide can we close quick create or not
@ -32,7 +31,9 @@
* @override
*/
_cancel: function () {
this.trigger_up("close_quick_create");
if (config.device.isMobile) {
this.trigger_up("close_quick_create");
}
},
/**
* Clear input when showed
@ -40,7 +41,7 @@
*/
toggleFold: function () {
this._super.apply(this, arguments);
if (!this.folded) {
if (config.device.isMobile && !this.folded) {
this.$input.val("");
}
},
@ -78,17 +79,24 @@
* @override
*/
on_attach_callback: function () {
if (
this._scrollPosition &&
this.state.groupedBy.length &&
this.widgets.length
) {
var $column = this.widgets[this.activeColumnIndex].$el;
$column.scrollLeft(this._scrollPosition.left);
$column.scrollTop(this._scrollPosition.top);
if (config.device.isMobile) {
if (
this._scrollPosition &&
this.state.groupedBy.length &&
this.widgets.length
) {
const $column = this.widgets[this.activeColumnIndex].$el;
$column.scrollLeft(this._scrollPosition.left);
$column.scrollTop(this._scrollPosition.top);
}
this._computeTabPosition();
}
this._computeTabPosition();
this._super.apply(this, arguments);
core.bus.on("UI_CONTEXT:IS_SMALL_CHANGED", this, () => {
this.widgets = [];
this.columnOptions.recordsDraggable = !config.device.isMobile;
this._renderView();
});
},
/**
* As this renderer defines its own scrolling area (the column in grouped
@ -99,7 +107,7 @@
*/
on_detach_callback: function () {
if (this.state.groupedBy.length && this.widgets.length) {
var $column = this.widgets[this.activeColumnIndex].$el;
const $column = this.widgets[this.activeColumnIndex].$el;
this._scrollPosition = {
left: $column.scrollLeft(),
top: $column.scrollTop(),
@ -107,6 +115,7 @@
} else {
this._scrollPosition = null;
}
core.bus.off("UI_CONTEXT:IS_SMALL_CHANGED", this);
this._super.apply(this, arguments);
},
@ -122,10 +131,17 @@
* @returns {Promise}
*/
addQuickCreate: function () {
if (this._canCreateColumn() && !this.quickCreate.folded) {
this._onMobileQuickCreateClicked();
if (config.device.isMobile) {
if (
this._canCreateColumn() &&
this.quickCreate &&
!this.quickCreate.folded
) {
this._onMobileQuickCreateClicked();
}
return this.widgets[this.activeColumnIndex].addQuickCreate();
}
return this.widgets[this.activeColumnIndex].addQuickCreate();
return this._super.apply(this, arguments);
},
/**
@ -135,23 +151,34 @@
* @override
*/
updateColumn: function (localID) {
var index = _.findIndex(this.widgets, {db_id: localID});
var $column = this.widgets[index].$el;
var scrollTop = $column.scrollTop();
return (
this._super
.apply(this, arguments)
.then(() => this._layoutUpdate(false))
// Required when clicking on 'Load More'
.then(() => $column.scrollTop(scrollTop))
.then(() => this._enableSwipe())
);
if (config.device.isMobile) {
const index = _.findIndex(this.widgets, {db_id: localID});
const $column = this.widgets[index].$el;
const scrollTop = $column.scrollTop();
return (
this._super
.apply(this, arguments)
.then(() => this._layoutUpdate(false))
// Required when clicking on 'Load More'
.then(() => $column.scrollTop(scrollTop))
.then(() => this._enableSwipe())
);
}
return this._super.apply(this, arguments);
},
// --------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------
/**
* Avoid drag'n'drop of kanban records on mobile and let the way to swipe
* @private
*/
_setState: function () {
const res = this._super.apply(this, arguments);
this.columnOptions.recordsDraggable = !config.device.isMobile;
return res;
},
/**
* Check if we use the quick create on mobile
* @returns {Boolean}
@ -225,11 +252,11 @@
*/
_computeCurrentColumn: function () {
if (this.widgets.length) {
var column = this.widgets[this.activeColumnIndex];
const column = this.widgets[this.activeColumnIndex];
if (!column) {
return;
}
var columnID = column.id || column.db_id;
const columnID = column.id || column.db_id;
this.$(
".o_kanban_mobile_tab.o_current, .o_kanban_group.o_current"
).removeClass("o_current");
@ -261,14 +288,14 @@
*/
_computeTabScrollPosition: function () {
if (this.widgets.length) {
var lastItemIndex = this.widgets.length - 1;
var moveToIndex = this.activeColumnIndex;
var scrollToLeft = 0;
for (var i = 0; i < moveToIndex; i++) {
var columnWidth = this._getTabWidth(this.widgets[i]);
const lastItemIndex = this.widgets.length - 1;
const moveToIndex = this.activeColumnIndex;
let scrollToLeft = 0;
for (let i = 0; i < moveToIndex; i++) {
const columnWidth = this._getTabWidth(this.widgets[i]);
// Apply
if (moveToIndex !== lastItemIndex && i === moveToIndex - 1) {
var partialWidth = 0.75;
const partialWidth = 0.75;
scrollToLeft += columnWidth * partialWidth;
} else {
scrollToLeft += columnWidth;
@ -287,13 +314,12 @@
*/
_computeTabJustification: function () {
if (this.widgets.length) {
var self = this;
// Use to compute the sum of the width of all tab
var widthChilds = this.widgets.reduce(function (total, column) {
return total + self._getTabWidth(column);
const widthChilds = this.widgets.reduce((total, column) => {
return total + this._getTabWidth(column);
}, 0);
// Apply a space around between child if the parent length is higher then the sum of the child width
var $tabs = this.$(".o_kanban_mobile_tabs");
const $tabs = this.$(".o_kanban_mobile_tabs");
$tabs.toggleClass(
"justify-content-between",
$tabs.outerWidth() >= widthChilds
@ -307,20 +333,25 @@
* @private
*/
_enableSwipe: function () {
var self = this;
var step = _t.database.parameters.direction === "rtl" ? -1 : 1;
const step = _t.database.parameters.direction === "rtl" ? -1 : 1;
this.$el.swipe({
excludedElements: ".o_kanban_mobile_tabs",
swipeLeft: function () {
var moveToIndex = self.activeColumnIndex + step;
if (moveToIndex < self.widgets.length) {
self._moveToGroup(moveToIndex, self.ANIMATE);
swipeLeft: () => {
if (!config.device.isMobile) {
return;
}
const moveToIndex = this.activeColumnIndex + step;
if (moveToIndex < this.widgets.length) {
this._moveToGroup(moveToIndex, this.ANIMATE);
}
},
swipeRight: function () {
var moveToIndex = self.activeColumnIndex - step;
swipeRight: () => {
if (!config.device.isMobile) {
return;
}
const moveToIndex = this.activeColumnIndex - step;
if (moveToIndex > -1) {
self._moveToGroup(moveToIndex, self.ANIMATE);
this._moveToGroup(moveToIndex, this.ANIMATE);
}
},
});
@ -334,7 +365,7 @@
* @private
*/
_getTabWidth: function (column) {
var columnID = column.id || column.db_id;
const columnID = column.id || column.db_id;
return this.$(
'.o_kanban_mobile_tab[data-id="' + columnID + '"]'
).outerWidth();
@ -366,16 +397,15 @@
if (this.widgets.length === 0) {
return Promise.resolve();
}
var self = this;
if (moveToIndex >= 0 && moveToIndex < this.widgets.length) {
this.activeColumnIndex = moveToIndex;
}
var column = this.widgets[this.activeColumnIndex];
const column = this.widgets[this.activeColumnIndex];
this._enableSwipe();
if (!column.data.isOpen) {
this.trigger_up("column_toggle_fold", {
db_id: column.db_id,
onSuccess: () => self._layoutUpdate(animate),
onSuccess: () => this._layoutUpdate(animate),
});
} else {
this._layoutUpdate(animate);
@ -388,39 +418,45 @@
*/
_renderExampleBackground: function () {
// Override to avoid display of example background
if (!config.device.isMobile) {
this._super.apply(this, arguments);
}
},
/**
* @override
* @private
*/
_renderGrouped: function (fragment) {
var self = this;
var newFragment = document.createDocumentFragment();
this._super.apply(this, [newFragment]);
this.defs.push(
Promise.all(this.defs).then(function () {
var data = [];
_.each(self.state.data, function (group) {
if (!group.value) {
group = _.extend({}, group, {value: _t("Undefined")});
data.unshift(group);
} else {
data.push(group);
}
});
if (config.device.isMobile) {
const newFragment = document.createDocumentFragment();
this._super.apply(this, [newFragment]);
this.defs.push(
Promise.all(this.defs).then(() => {
const data = [];
_.each(this.state.data, function (group) {
if (!group.value) {
group = _.extend({}, group, {value: _t("Undefined")});
data.unshift(group);
} else {
data.push(group);
}
});
var kanbanColumnContainer = document.createElement("div");
kanbanColumnContainer.classList.add("o_kanban_columns_content");
kanbanColumnContainer.appendChild(newFragment);
fragment.appendChild(kanbanColumnContainer);
$(
qweb.render("KanbanView.MobileTabs", {
data: data,
quickCreateEnabled: self._canCreateColumn(),
})
).prependTo(fragment);
})
);
const kanbanColumnContainer = document.createElement("div");
kanbanColumnContainer.classList.add("o_kanban_columns_content");
kanbanColumnContainer.appendChild(newFragment);
fragment.appendChild(kanbanColumnContainer);
$(
qweb.render("KanbanView.MobileTabs", {
data: data,
quickCreateEnabled: this._canCreateColumn(),
})
).prependTo(fragment);
})
);
} else {
this._super.apply(this, arguments);
}
},
/**
@ -428,14 +464,17 @@
* @private
*/
_renderView: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
if (self.state.groupedBy.length) {
const def = this._super.apply(this, arguments);
if (!config.device.isMobile) {
return def;
}
return def.then(() => {
if (this.state.groupedBy.length) {
// Force first column for kanban view, because the groupedBy can be changed
return self._moveToGroup(0);
return this._moveToGroup(0);
}
if (self._canCreateColumn()) {
self._onMobileQuickCreateClicked();
if (this._canCreateColumn()) {
this._onMobileQuickCreateClicked();
}
return Promise.resolve();
});
@ -516,7 +555,7 @@
* @override
*/
_onCloseQuickCreate: function () {
if (this.widgets.length && !this.quickCreate.folded) {
if (this.widgets.length && this.quickCreate && !this.quickCreate.folded) {
this.$(".o_kanban_group").toggle(true);
this.quickCreate.toggleFold();
}

View File

@ -0,0 +1,144 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2021 ITerra - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define("web_responsive", function (require) {
"use strict";
const config = require("web.config");
const core = require("web.core");
const FormRenderer = require("web.FormRenderer");
const RelationalFields = require("web.relational_fields");
const ListRenderer = require("web.ListRenderer");
const CalendarRenderer = require("web.CalendarRenderer");
// 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)
);
RelationalFields.FieldStatus.include({
/**
* Fold all on mobiles.
*
* @override
*/
_setState: function () {
this._super.apply(this, arguments);
if (config.device.isMobile) {
_.map(this.status_information, (value) => {
value.fold = true;
});
}
},
});
// Sticky Column Selector
ListRenderer.include({
_renderView: function () {
return this._super.apply(this, arguments).then(() => {
const $col_selector = this.$el.find(
".o_optional_columns_dropdown_toggle"
);
if ($col_selector.length !== 0) {
const $th = this.$el.find("thead>tr:first>th:last");
$col_selector.appendTo($th);
}
});
},
_onToggleOptionalColumnDropdown: function (ev) {
// FIXME: For some strange reason the 'stopPropagation' call
// in the main method don't work. Invoking here the same method
// does the expected behavior... O_O!
// This prevents the action of sorting the column from being
// launched.
ev.stopPropagation();
this._super.apply(this, arguments);
},
});
// Responsive view "action" buttons
FormRenderer.include({
/**
* @override
*/
on_attach_callback: function () {
this._super.apply(this, arguments);
core.bus.on("UI_CONTEXT:IS_SMALL_CHANGED", this, () => {
this._applyFormSizeClass();
this._render();
});
},
/**
* @override
*/
on_detach_callback: function () {
core.bus.off("UI_CONTEXT:IS_SMALL_CHANGED", this);
this._super.apply(this, arguments);
},
/**
* In mobiles, put all statusbar buttons in a dropdown.
*
* @override
*/
_renderHeaderButtons: function () {
const $buttons = this._super.apply(this, arguments);
if (
!config.device.isMobile ||
$buttons.children("button:not(.o_invisible_modifier)").length <= 2
) {
return $buttons;
}
// $buttons must be appended by JS because all events are bound
const $dropdown = $(
core.qweb.render("web_responsive.MenuStatusbarButtons")
);
$buttons.addClass("dropdown-menu").appendTo($dropdown);
return $dropdown;
},
});
CalendarRenderer.include({
/**
* @override
*/
on_attach_callback: function () {
this._super.apply(this, arguments);
core.bus.on("UI_CONTEXT:IS_SMALL_CHANGED", this, () => {
// Hack to force calendar to reload their options and rerender
this.calendar.setOption("locale", moment.locale());
});
},
/**
* @override
*/
on_detach_callback: function () {
core.bus.off("UI_CONTEXT:IS_SMALL_CHANGED", this);
this._super.apply(this, arguments);
},
/**
* @override
*/
_getFullCalendarOptions: function () {
const options = this._super.apply(this, arguments);
Object.defineProperty(options.views.dayGridMonth, "columnHeaderFormat", {
get() {
return config.device.isMobile ? "ddd" : "dddd";
},
});
return options;
},
});
});

View File

@ -3,6 +3,7 @@
Copyright 2017 LasLabs Inc.
Copyright 2018 Alexandre Díaz
Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2021 ITerra - Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates id="form_view" xml:space="preserve">

View File

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t t-extend="AppsMenu">
<!-- App icons must be clickable -->
<t t-jquery=".o_app" t-operation="attributes">
<attribute
name="t-attf-href"
t-translation="off"
>#menu_id=#{app.menuID}&amp;action_id=#{app.actionID}</attribute>
<attribute name="draggable" t-translation="off">false</attribute>
</t>
<!-- App icons should be more than a text -->
<t t-jquery=".o_app &gt; t" t-operation="replace">
<t t-call="web_responsive.AppIcon" />
</t>
<!-- Same hotkey as in EE -->
<t t-jquery=".full" t-operation="attributes">
<attribute name="accesskey">a</attribute>
</t>
<!-- Search bar -->
<t t-jquery="[t-as=app]" t-operation="before">
<div class="search-container align-items-center col-12">
<div class="search-input">
<div class="input-group">
<div class="input-group-prepend">
<span class="fa fa-search" />
</div>
<input
type="search"
autocomplete="off"
placeholder="Search menus..."
class="form-control"
/>
</div>
<div class="search-results ml-auto mr-auto rounded" />
</div>
</div>
</t>
</t>
<!-- Separate app icon template, for easier inheritance -->
<t t-name="web_responsive.AppIcon">
<img
class="o-app-icon"
draggable="false"
t-attf-src="data:image/png;base64,#{app.web_icon_data}"
/>
<span class="o-app-name">
<t t-esc="app.name" />
</span>
</t>
<!-- A search result -->
<t t-name="web_responsive.MenuSearchResults">
<t t-foreach="results" t-as="result">
<t t-set="menu" t-value="widget._menuInfo(result.original)" />
<a
t-attf-class="o-menu-search-result dropdown-item col-12 ml-auto mr-auto #{result_first ? 'active' : ''}"
t-attf-style="background-image:url('data:image/png;base64,#{menu.web_icon_data}')"
t-attf-href="#menu_id=#{menu.id}&amp;action_id=#{menu.action_id}"
t-att-data-menu-id="menu.id"
t-att-data-action-id="menu.action_id"
t-att-data-parent-id="menu.parent_id[0]"
draggable="false"
t-raw="result.string"
/>
</t>
</t>
</template>

View File

@ -1,146 +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.ControlPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//nav[hasclass('o_cp_switch_buttons')]" position="replace">
<nav
t-if="props.views.length gt 1"
class="btn-group o_cp_switch_buttons"
role="toolbar"
aria-label="View switcher"
>
<t
t-set="collapse_switchview"
t-value="device.size_class &lt;= device.SIZES.LG"
/>
<button
t-if="collapse_switchview"
class="btn btn-link btn-sm"
data-toggle="dropdown"
aria-expanded="false"
>
<span
t-attf-class="fa fa-lg o_switch_view o_{{ env.view.type }} {{ props.views.filter(view => view.type === env.view.type)[0].icon }}"
/>
</button>
<ul
t-if="collapse_switchview"
class="dropdown-menu dropdown-menu-right list-inline"
>
<li t-foreach="props.views" t-as="view" t-key="view.type">
<t t-call="web.ViewSwitcherButton" />
</li>
</ul>
<t
t-if="!collapse_switchview"
t-foreach="props.views"
t-as="view"
t-key="view.type"
>
<t t-call="web.ViewSwitcherButton" />
</t>
</nav>
</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 = device.isMobile ? 'quick' : ''"
>
<t t-if="!device.isMobile">
<i
class="o_searchview_icon fa fa-search"
title="Search..."
role="img"
aria-label="Search..."
/>
<SearchBar fields="fields" />
</t>
<t t-if="device.isMobile and state.mobileSearchMode == 'quick'">
<button
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="device.isMobile and state.mobileSearchMode == 'full'"
t-call="web_responsive.MobileSearchView"
/>
<t t-if="device.isMobile and state.mobileSearchMode == ''">
<button
class="btn btn-link fa fa-search"
t-on-click.stop="state.mobileSearchMode = 'quick'"
/>
</t>
</div>
</xpath>
<xpath expr="//div[hasclass('o_cp_top_left')]" position="attributes">
<attribute
name="t-att-class"
t-translation="off"
>device.isMobile and state.mobileSearchMode == 'quick' ? 'o_hidden' : ''</attribute>
</xpath>
<xpath expr="//div[hasclass('o_search_options')]" position="attributes">
<attribute name="t-if" t-translation="off">!device.isMobile</attribute>
<attribute
name="t-att-class"
t-translation="off"
>device.size_class == device.SIZES.MD ? 'o_search_options_hide_labels' : ''</attribute>
</xpath>
</t>
<t t-name="web_responsive.MobileSearchView" owl="1">
<div class="o_mobile_search">
<div class="o_mobile_search_header">
<span
class="o_mobile_search_close float-left mt16 mb16 mr8 ml16"
t-on-click.stop="state.mobileSearchMode = '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="model.dispatch('clearQuery')"
>
<t>CLEAR</t>
</span>
</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.stop="state.mobileSearchMode = ''"
>
<t>SEE RESULT</t>
</div>
</div>
</t>
</templates>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2019 Tecnativa - Alexandre Díaz
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t t-extend="mail.discuss.ControlButtons">
<t
t-jquery=".o_mail_discuss_button_multi_user_channel"
t-operation="attributes"
>
<attribute name="class" t-translation="off">
btn btn-secondary o_mail_discuss_button_multi_user_channel d-md-block
d-none
</attribute>
</t>
</t>
</template>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<div t-extend="UserMenu.shortcuts">
<t
t-jquery="table.o_shortcut_table > tbody > tr > td:nth-child(2) > span:first-child"
t-operation="after"
>
+ <span class="o_key">Shift</span>
</t>
</div>
</templates>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2017-2018 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t t-extend="Menu">
<t t-jquery=".o_menu_apps" t-operation="after">
<!-- Hamburger button to show submenus in sm screens -->
<button
class="o-menu-toggle d-md-none"
data-toggle="collapse"
data-target=".o_main_navbar .o_menu_sections"
>
<i class="fa fa-bars" />
</button>
</t>
</t>
</templates>

View File

@ -7,7 +7,8 @@ from odoo.tests import common
class TestResUsers(common.TransactionCase):
def test_chatter_position_wr(self):
user_public = self.env.ref("base.public_user")
user_public = user_public.with_user(user_public)
self.assertEqual(user_public.chatter_position, "sided")
user_public.with_user(user_public).write({"chatter_position": "normal"})
user_public.write({"chatter_position": "normal"})
self.assertEqual(user_public.chatter_position, "normal")

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2016 LasLabs Inc.
@author Dave Lasley <dave@laslabs.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<template
id="assets_backend"
name="Open Mobile Assets"
inherit_id="web.assets_backend"
>
<xpath expr="//link[last()]" position="after">
<link
rel="stylesheet"
href="/web_responsive/static/src/css/web_responsive.scss"
/>
<link
rel="stylesheet"
href="/web_responsive/static/src/css/search_view_mobile.scss"
/>
<link
rel="stylesheet"
href="/web_responsive/static/src/css/kanban_view_mobile.scss"
/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script
type="application/javascript"
src="/web_responsive/static/src/js/web_responsive.js"
/>
<script
type="application/javascript"
src="/web_responsive/static/src/js/kanban_renderer_mobile.js"
/>
</xpath>
</template>
</odoo>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2018 Alexandre Díaz
Copyright 2021 ITerra - Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>