diff --git a/README.md b/README.md index b2295b762d60..d1452e61473e 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,9 @@ addon | version | maintainers | summary [web_sort_menu](web_sort_menu/) | 18.0.1.0.0 | | Sort Apps in DropDown/NavBar Menu alphabetically [web_systray_button_init_action](web_systray_button_init_action/) | 18.0.1.0.2 | | Add a button to go to the user init action. [web_theme_classic](web_theme_classic/) | 18.0.1.1.0 | legalsylvain | Contrasted style on fields to improve the UI. +[web_time_range_menu_custom](web_time_range_menu_custom/) | 18.0.1.0.0 | | Web Time Range Menu Custom [web_timeline](web_timeline/) | 18.0.1.0.2 | tarteo | Interactive visualization chart to show events in time +[web_toggle_chatter](web_toggle_chatter/) | 18.0.1.0.0 | | Toggle chatter in backend form views [web_touchscreen](web_touchscreen/) | 18.0.1.0.0 | yajo rafaelbn | UX improvements for touch screens [web_tree_column_keyboard_resize](web_tree_column_keyboard_resize/) | 18.0.1.0.0 | | Allow resizing tree view columns using keyboard shortcuts [web_tree_dynamic_colored_field](web_tree_dynamic_colored_field/) | 18.0.1.0.1 | | Allows you to dynamically color fields on tree views diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index 903daa751720..3ec909679fff 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "odoo-addons-oca-web" -version = "18.0.20260115.0" +version = "18.0.20260224.0" dependencies = [ "odoo-addon-web_calendar_slot_duration==18.0.*", "odoo-addon-web_chatter_position==18.0.*", @@ -39,7 +39,9 @@ dependencies = [ "odoo-addon-web_sort_menu==18.0.*", "odoo-addon-web_systray_button_init_action==18.0.*", "odoo-addon-web_theme_classic==18.0.*", + "odoo-addon-web_time_range_menu_custom==18.0.*", "odoo-addon-web_timeline==18.0.*", + "odoo-addon-web_toggle_chatter==18.0.*", "odoo-addon-web_touchscreen==18.0.*", "odoo-addon-web_tree_column_keyboard_resize==18.0.*", "odoo-addon-web_tree_dynamic_colored_field==18.0.*", diff --git a/web_time_range_menu_custom/README.rst b/web_time_range_menu_custom/README.rst new file mode 100644 index 000000000000..157202ab155d --- /dev/null +++ b/web_time_range_menu_custom/README.rst @@ -0,0 +1,107 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +========================== +Web Time Range Menu Custom +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a1ba5362f4135173474450609ab05ffa31b197e6bf6f33c984f4b1d3e49c43c7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github + :target: https://github.com/OCA/web/tree/18.0/web_time_range_menu_custom + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_time_range_menu_custom + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Extend period and comparison period options for the date and datetime +fields filted menu adding a new option called "Custom Period". + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To see this module working: + +1. Navigate to any menu that supports date-based filters. +2. Open **Custom period** tab. +3. Add new date filter with the provided options. + +|Custom Period Option| + +For the pivots, on the comparison tab you can see the same option to +make the comparison with the provided filter, taking th reference from +the set filter. + +**Note:** For "days," it functions as a range; for example, "Last 7 +days" returns the period from 7 days ago up to today. However, for other +options, it provides the range for the selected period. For instance, +"Last 1 month" returns the period from the first day of the previous +month to the last day of the previous month. + +.. |Custom Period Option| image:: https://raw.githubusercontent.com/web_time_range_menu_custom/static/src/description/custom_period_option.png + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__: + + - Alexandre D. Díaz + - Carlos Roca + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/web `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_time_range_menu_custom/__init__.py b/web_time_range_menu_custom/__init__.py new file mode 100644 index 000000000000..ef5ae3587f59 --- /dev/null +++ b/web_time_range_menu_custom/__init__.py @@ -0,0 +1 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). diff --git a/web_time_range_menu_custom/__manifest__.py b/web_time_range_menu_custom/__manifest__.py new file mode 100644 index 000000000000..15f5f1caae33 --- /dev/null +++ b/web_time_range_menu_custom/__manifest__.py @@ -0,0 +1,23 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +{ + "name": "Web Time Range Menu Custom", + "version": "18.0.1.0.0", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/web", + "depends": ["web"], + "installable": True, + "auto_install": False, + "assets": { + "web.assets_backend": [ + ( + "after", + "/web/static/src/search/utils/dates.js", + "/web_time_range_menu_custom/static/src/js/*.esm.js", + ), + "/web_time_range_menu_custom/static/src/scss/*.scss", + "/web_time_range_menu_custom/static/src/xml/*.xml", + ], + }, +} diff --git a/web_time_range_menu_custom/i18n/es.po b/web_time_range_menu_custom/i18n/es.po new file mode 100644 index 000000000000..e0742b46ec25 --- /dev/null +++ b/web_time_range_menu_custom/i18n/es.po @@ -0,0 +1,87 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_time_range_menu_custom +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-19 19:33+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\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.17\n" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Add" +msgstr "Añadir" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Custom period" +msgstr "Período personalizado" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Day" +msgstr "Día" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Month" +msgstr "Mes" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Week" +msgstr "Semana" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Year" +msgstr "Año" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "day" +msgstr "" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "month" +msgstr "" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "week" +msgstr "" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "year" +msgstr "" diff --git a/web_time_range_menu_custom/i18n/it.po b/web_time_range_menu_custom/i18n/it.po new file mode 100644 index 000000000000..e985bd2435f0 --- /dev/null +++ b/web_time_range_menu_custom/i18n/it.po @@ -0,0 +1,87 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_time_range_menu_custom +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-11-20 13:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\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 5.6.2\n" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Add" +msgstr "Aggiungi" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Custom period" +msgstr "Periodo predefinito" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Day" +msgstr "Giorno" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Month" +msgstr "Mese" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Week" +msgstr "Settimana" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "Year" +msgstr "Anno" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "day" +msgstr "giorno" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "month" +msgstr "mese" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "week" +msgstr "settimana" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +#, python-format +msgid "year" +msgstr "anno" diff --git a/web_time_range_menu_custom/i18n/web_time_range_menu_custom.pot b/web_time_range_menu_custom/i18n/web_time_range_menu_custom.pot new file mode 100644 index 000000000000..62aac10e2527 --- /dev/null +++ b/web_time_range_menu_custom/i18n/web_time_range_menu_custom.pot @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_time_range_menu_custom +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +msgid "Add" +msgstr "" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +msgid "Custom period" +msgstr "" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +msgid "Day" +msgstr "" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +msgid "Month" +msgstr "" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +msgid "Week" +msgstr "" + +#. module: web_time_range_menu_custom +#. odoo-javascript +#: code:addons/web_time_range_menu_custom/static/src/xml/date_selector.xml:0 +msgid "Year" +msgstr "" diff --git a/web_time_range_menu_custom/pyproject.toml b/web_time_range_menu_custom/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/web_time_range_menu_custom/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/web_time_range_menu_custom/readme/CONTRIBUTORS.md b/web_time_range_menu_custom/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..2ddacdb2d893 --- /dev/null +++ b/web_time_range_menu_custom/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Tecnativa](https://www.tecnativa.com): + - Alexandre D. Díaz + - Carlos Roca diff --git a/web_time_range_menu_custom/readme/DESCRIPTION.md b/web_time_range_menu_custom/readme/DESCRIPTION.md new file mode 100644 index 000000000000..df76463aec26 --- /dev/null +++ b/web_time_range_menu_custom/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +Extend period and comparison period options for the date and datetime +fields filted menu adding a new option called "Custom Period". diff --git a/web_time_range_menu_custom/readme/USAGE.md b/web_time_range_menu_custom/readme/USAGE.md new file mode 100644 index 000000000000..703ff8b4e0b0 --- /dev/null +++ b/web_time_range_menu_custom/readme/USAGE.md @@ -0,0 +1,17 @@ +To see this module working: + +1. Navigate to any menu that supports date-based filters. +2. Open **Custom period** tab. +3. Add new date filter with the provided options. + +![Custom Period Option](/web_time_range_menu_custom/static/src/description/custom_period_option.png) + +For the pivots, on the comparison tab you can see the same option to +make the comparison with the provided filter, taking th reference from +the set filter. + +**Note:** For "days," it functions as a range; for example, "Last 7 +days" returns the period from 7 days ago up to today. However, for other +options, it provides the range for the selected period. For instance, +"Last 1 month" returns the period from the first day of the previous +month to the last day of the previous month. diff --git a/web_time_range_menu_custom/static/description/custom_period_option.png b/web_time_range_menu_custom/static/description/custom_period_option.png new file mode 100644 index 000000000000..c19c93488adb Binary files /dev/null and b/web_time_range_menu_custom/static/description/custom_period_option.png differ diff --git a/web_time_range_menu_custom/static/description/icon.png b/web_time_range_menu_custom/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/web_time_range_menu_custom/static/description/icon.png differ diff --git a/web_time_range_menu_custom/static/description/index.html b/web_time_range_menu_custom/static/description/index.html new file mode 100644 index 000000000000..325adac37d2b --- /dev/null +++ b/web_time_range_menu_custom/static/description/index.html @@ -0,0 +1,453 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Web Time Range Menu Custom

+ +

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runboat

+

Extend period and comparison period options for the date and datetime +fields filted menu adding a new option called “Custom Period”.

+

Table of contents

+ +
+

Usage

+

To see this module working:

+
    +
  1. Navigate to any menu that supports date-based filters.
  2. +
  3. Open Custom period tab.
  4. +
  5. Add new date filter with the provided options.
  6. +
+

Custom Period Option

+

For the pivots, on the comparison tab you can see the same option to +make the comparison with the provided filter, taking th reference from +the set filter.

+

Note: For “days,” it functions as a range; for example, “Last 7 +days” returns the period from 7 days ago up to today. However, for other +options, it provides the range for the selected period. For instance, +“Last 1 month” returns the period from the first day of the previous +month to the last day of the previous month.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:
      +
    • Alexandre D. Díaz
    • +
    • Carlos Roca
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/web project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/web_time_range_menu_custom/static/src/js/date_selector.esm.js b/web_time_range_menu_custom/static/src/js/date_selector.esm.js new file mode 100644 index 000000000000..8554c126d9eb --- /dev/null +++ b/web_time_range_menu_custom/static/src/js/date_selector.esm.js @@ -0,0 +1,117 @@ +/* Copyright 2022 Tecnativa - Carlos Roca + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ +import {useBus} from "@web/core/utils/hooks"; + +const {Component, useState} = owl; +import * as dates from "@web/search/utils/dates"; +import {customPeriods} from "./dates.esm"; +const {DateTime} = luxon; +var ID_CUSTOM_DATE = 0; + +/** + * @extends Component + */ +export class DropdownItemCustomPeriod extends Component { + setup() { + this.isOpen = useState({value: false}); + this.type = useState({value: "day"}); + this.quantity = useState({value: 0}); + this.referenceMoment = DateTime.local(); + var fieldsSelected = new Set(); + for (var selected in this.props.comparisonItems) { + fieldsSelected.add(this.props.comparisonItems[selected].dateFilterId); + } + this.fields_selected = Array.from(fieldsSelected); + this.field = useState({ + value: (this.props.field && this.props.field.id) || this.fields_selected[0], + }); + + useBus(this.env.searchModel, "update", this.render); + } + + onClickCustomPeriod() { + this.isOpen.value = !this.isOpen.value; + } + + onClickAdd() { + if (this.props.type === "filter") { + this.addRange(); + } else if (this.props.type === "comparison") { + this.addComparison(); + } + } + + addRange() { + const field_id = this.field.value; + const key = "custom_" + ID_CUSTOM_DATE++; + const plusParamID = this.type.value + "s"; + var formatSearch = ""; + switch (this.type.value) { + case "day": + formatSearch = "dd MMMM yyyy"; + break; + case "week": + formatSearch = "'W'WW yyyy"; + break; + case "month": + formatSearch = "MMMM"; + break; + case "year": + formatSearch = "yyyy"; + break; + } + const periodSearch = { + id: key, + groupNumber: 3, + description: "Last " + this.quantity.value + " " + this.type.value + "s", + format: formatSearch, + plusParam: {[plusParamID]: -this.quantity.value}, + granularity: this.type.value, + defaultYearId: "year", + custom_period: { + is_custom_period: true, + last_period: this.quantity.value, + }, + }; + customPeriods.push(periodSearch); + this.env.searchModel.getSearchItems((f) => f.type === "field"); + this.env.searchModel.toggleDateFilter(field_id, key); + } + + addComparison() { + const key = "custom_" + ID_CUSTOM_DATE++; + const plusParamID = this.type.value + "s"; + Object.assign(dates.COMPARISON_OPTIONS, { + [key]: { + description: + "Previous " + this.quantity.value + " " + this.type.value + "s", + id: key, + plusParam: {[plusParamID]: -this.quantity.value}, + }, + }); + const comparisonId = this.env.searchModel.nextId; + this.env.searchModel.searchItems[comparisonId] = { + comparisonOptionId: key, + dateFilterId: this.field.value, + description: + this.env.searchModel.searchItems[this.field.value].description + + ": Previous " + + this.quantity.value + + " " + + this.type.value + + "s", + groupId: 14, + id: comparisonId, + type: "comparison", + }; + this.env.searchModel.nextId++; + this.env.searchModel.toggleSearchItem(comparisonId); + } +} +DropdownItemCustomPeriod.template = + "web_time_range_menu_custom.DropdownItemCustomPeriod"; +DropdownItemCustomPeriod.props = { + type: {type: String}, + field: {type: Object, optional: true}, + comparisonItems: {type: Object, optional: true}, +}; diff --git a/web_time_range_menu_custom/static/src/js/dates.esm.js b/web_time_range_menu_custom/static/src/js/dates.esm.js new file mode 100644 index 000000000000..f34320133d1f --- /dev/null +++ b/web_time_range_menu_custom/static/src/js/dates.esm.js @@ -0,0 +1,358 @@ +/* eslint-disable init-declarations */ +/* Copyright 2022 Tecnativa - Carlos Roca + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ + +import {patch} from "@web/core/utils/patch"; +import {pick} from "@web/core/utils/objects"; +import {Domain} from "@web/core/domain"; +import {serializeDate, serializeDateTime} from "@web/core/l10n/dates"; +import {localization} from "@web/core/l10n/localization"; +/* Redefine some methods of dates.js from web.static.src.utils to +add support to days and weeks periods */ +import * as dates from "@web/search/utils/dates"; + +/** + * Method used to calculate the quantity of days and weeks of the + * actual year. + */ +function _getQtyOfCurrentYear(option) { + const now = new Date(); + const startOfYear = new Date(now.getFullYear(), 0, 1); + const endOfYear = new Date(now.getFullYear(), 11, 31); + let millisecondsPerWeek = 0; + if (option === "week") { + millisecondsPerWeek = 604800000; + } else if (option === "day") { + millisecondsPerWeek = 86400000; + } else { + return; + } + const weeks = Math.ceil((endOfYear - startOfYear) / millisecondsPerWeek); + return weeks; +} + +Object.assign(dates.PER_YEAR, { + week: _getQtyOfCurrentYear("week"), + day: _getQtyOfCurrentYear("day"), +}); + +export const customPeriods = []; +// This is needed to call the super functions on @web/search/utils/dates +const _getSetParam = dates.getSetParam; +const _getPeriodOptions = dates.getPeriodOptions; + +// Patch of functions defined before +patch(dates, { + /* + * Redefine function to avoid the exclusion of days and weeks. + */ + constructDateDomain( + referenceMoment, + searchItem, + selectedOptionIds, + comparisonOptionId + ) { + let plusParam; + let selectedOptions; + if (comparisonOptionId) { + [plusParam, selectedOptions] = dates.getComparisonParams( + referenceMoment, + searchItem, + selectedOptionIds, + comparisonOptionId + ); + } else { + selectedOptions = dates.getSelectedOptions( + referenceMoment, + searchItem, + selectedOptionIds + ); + } + if ("withDomain" in selectedOptions) { + return { + description: selectedOptions.withDomain[0].description, + domain: Domain.and([ + selectedOptions.withDomain[0].domain, + searchItem.domain, + ]), + }; + } + const yearOptions = selectedOptions.year; + const otherOptions = [ + ...(selectedOptions.quarter || []), + ...(selectedOptions.month || []), + ...(selectedOptions.day || []), + ...(selectedOptions.week || []), + ]; + dates.sortPeriodOptions(yearOptions); + dates.sortPeriodOptions(otherOptions); + const ranges = []; + const {fieldName, fieldType} = searchItem; + for (const yearOption of yearOptions) { + const constructRangeParams = { + referenceMoment, + fieldName, + fieldType, + plusParam, + }; + if (otherOptions.length) { + for (const option of otherOptions) { + const year_param = + option.granularity === "week" + ? {weekYear: yearOption.setParam.year} + : yearOption.setParam; + const setParam = Object.assign( + {}, + year_param, + option ? option.setParam : {} + ); + const {granularity, custom_period} = option; + if (comparisonOptionId && custom_period.is_custom_period) { + custom_period.is_comparison = true; + } + const range = dates.constructDateRange( + Object.assign( + {granularity, custom_period, setParam}, + constructRangeParams + ) + ); + ranges.push(range); + } + } else { + const {granularity, custom_period, setParam} = yearOption; + if (comparisonOptionId && custom_period.is_custom_period) { + custom_period.is_comparison = true; + } + const range = dates.constructDateRange( + Object.assign( + {granularity, custom_period, setParam}, + constructRangeParams + ) + ); + ranges.push(range); + } + } + const domain = Domain.combine( + ranges.map((range) => range.domain), + "OR" + ); + const description = ranges.map((range) => range.description).join("/"); + return {domain, description}; + }, + constructDateRange(params) { + const { + referenceMoment, + fieldName, + fieldType, + granularity, + setParam, + plusParam, + custom_period, + } = params; + if ("quarter" in setParam) { + // Luxon does not consider quarter key in setParam (like moment did) + setParam.month = dates.QUARTERS[setParam.quarter].coveredMonths[0]; + delete setParam.quarter; + } + const date = referenceMoment.set(setParam).plus(plusParam || {}); + // Compute domain + var leftDate = date.startOf(granularity); + var rightDate = date.endOf(granularity); + if ( + custom_period.is_custom_period && + parseInt(custom_period.last_period) !== 0 + ) { + if (granularity === "day") { + const plusParamReferenceMoment = referenceMoment.plus(plusParam || {}); + leftDate = leftDate.plus({days: 1}); + rightDate = plusParamReferenceMoment; + } else { + var customPlusParam = {}; + customPlusParam[granularity + "s"] = parseInt( + custom_period.last_period + ); + rightDate = leftDate.plus(customPlusParam); + } + } + let leftBound; + let rightBound; + if (fieldType === "date") { + leftBound = serializeDate(leftDate); + rightBound = serializeDate(rightDate); + } else { + leftBound = serializeDateTime(leftDate); + rightBound = serializeDateTime(rightDate); + } + const domain = new Domain([ + "&", + [fieldName, ">=", leftBound], + [fieldName, "<=", rightBound], + ]); + // Compute description + var description = ""; + if (custom_period.is_custom_period) { + if (plusParam) { + const key = Object.keys(plusParam)[0]; + description = "Previous " + Math.abs(plusParam[key]) + " " + key; + } else { + description = + "Last " + custom_period.last_period + " " + granularity + "s"; + } + } else { + var descriptions = [date.toFormat("yyyy")]; + const method = localization.direction === "rtl" ? "push" : "unshift"; + if (granularity === "month") { + descriptions[method](date.toFormat("MMMM")); + } else if (granularity === "quarter") { + const quarter = date.quarter; + descriptions[method](dates.QUARTERS[quarter].description.toString()); + } else if (granularity === "day") { + descriptions[method](date.toFormat("dd MMMM")); + } else if (granularity === "week") { + descriptions[method](date.toFormat("'W'WW")); + } + description = descriptions.join(" "); + } + return {domain, description}; + }, + getPeriodOptions(referenceMoment, optionsParams) { + const res = _getPeriodOptions(referenceMoment, optionsParams); + return res.concat(customPeriods); + }, + /* + * Add selection of month when week or day selected. + * + * @override + */ + getSetParam(periodOption, referenceMoment) { + if (periodOption.granularity === "day") { + const date = referenceMoment.plus(periodOption.plusParam); + return { + day: date.day, + month: date.month, + }; + } else if (periodOption.granularity === "week") { + const date = referenceMoment.plus(periodOption.plusParam); + return { + weekNumber: date.weekNumber, + }; + } + return _getSetParam(...arguments); + }, + getSelectedOptions(referenceMoment, searchItem, selectedOptionIds) { + const selectedOptions = {year: []}; + const periodOptions = dates.getPeriodOptions( + referenceMoment, + searchItem.optionsParams + ); + for (const optionId of selectedOptionIds) { + const option = periodOptions.find((option) => option.id === optionId); + const custom_period = option.custom_period || {}; + const granularity = option.granularity; + if (!selectedOptions[granularity]) { + selectedOptions[granularity] = []; + } + if (option.domain) { + selectedOptions[granularity].push( + pick(option, "domain", "description") + ); + } else { + const setParam = dates.getSetParam(option, referenceMoment); + selectedOptions[granularity].push({ + granularity, + setParam, + custom_period, + }); + } + } + return selectedOptions; + }, + /* + * Add support to day and week options. + * + */ + getComparisonParams( + referenceMoment, + searchItem, + selectedOptionIds, + comparisonOptionId + ) { + const comparisonOption = dates.COMPARISON_OPTIONS[comparisonOptionId]; + const selectedOptions = dates.getSelectedOptions( + referenceMoment, + searchItem, + selectedOptionIds + ); + if (comparisonOption.plusParam) { + return [comparisonOption.plusParam, selectedOptions]; + } + const plusParam = {}; + let globalGranularity = "year"; + if (selectedOptions.day) { + globalGranularity = "day"; + } else if (selectedOptions.week) { + globalGranularity = "week"; + } else if (selectedOptions.month) { + globalGranularity = "month"; + } else if (selectedOptions.quarter) { + globalGranularity = "quarter"; + } + const granularityFactor = dates.PER_YEAR[globalGranularity]; + const years = selectedOptions.year.map((o) => o.setParam.year); + const yearMin = Math.min(...years); + const yearMax = Math.max(...years); + let optionMin = 0; + let optionMax = 0; + if (selectedOptions.quarter) { + const quarters = selectedOptions.quarter.map((o) => o.setParam.quarter); + if (globalGranularity === "month") { + delete selectedOptions.quarter; + for (const quarter of quarters) { + for (const month of dates.QUARTERS[quarter].coveredMonths) { + const monthOption = selectedOptions.month.find( + (o) => o.setParam.month === month + ); + if (!monthOption) { + selectedOptions.month.push({ + setParam: {month}, + granularity: "month", + }); + } + } + } + } else { + optionMin = Math.min(...quarters); + optionMax = Math.max(...quarters); + } + } + if (selectedOptions.month) { + const months = selectedOptions.month.map((o) => o.setParam.month); + optionMin = Math.min(...months); + optionMax = Math.max(...months); + } + if (selectedOptions.week) { + const weeks = selectedOptions.week.map((o) => o.setParam.weekNumber); + optionMin = Math.min(...weeks); + optionMax = Math.max(...weeks); + } + if (selectedOptions.day) { + const days = selectedOptions.day.map((o) => o.setParam.day); + optionMin = Math.min(...days); + optionMax = Math.max(...days); + } + const num = + -1 + granularityFactor * (yearMin - yearMax) + optionMin - optionMax; + const key = + globalGranularity === "year" + ? "years" + : globalGranularity === "month" + ? "months" + : globalGranularity === "week" + ? "weeks" + : globalGranularity === "day" + ? "days" + : "quarters"; + plusParam[key] = num; + return [plusParam, selectedOptions]; + }, +}); diff --git a/web_time_range_menu_custom/static/src/js/search_bar_menu.esm.js b/web_time_range_menu_custom/static/src/js/search_bar_menu.esm.js new file mode 100644 index 000000000000..3e2b936ff818 --- /dev/null +++ b/web_time_range_menu_custom/static/src/js/search_bar_menu.esm.js @@ -0,0 +1,12 @@ +/* Copyright 2022 Tecnativa - Carlos Roca + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ +import {patch} from "@web/core/utils/patch"; +import {SearchBarMenu} from "@web/search/search_bar_menu/search_bar_menu"; +import {DropdownItemCustomPeriod} from "./date_selector.esm"; + +patch(SearchBarMenu, { + components: { + ...SearchBarMenu.components, + DropdownItemCustomPeriod, + }, +}); diff --git a/web_time_range_menu_custom/static/src/js/search_model.esm.js b/web_time_range_menu_custom/static/src/js/search_model.esm.js new file mode 100644 index 000000000000..53d363d0844d --- /dev/null +++ b/web_time_range_menu_custom/static/src/js/search_model.esm.js @@ -0,0 +1,37 @@ +/* Copyright 2025 Tecnativa - Carlos Roca + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ +import {SearchModel} from "@web/search/search_model"; +import {patch} from "@web/core/utils/patch"; +import {getPeriodOptions} from "@web/search/utils/dates"; + +patch(SearchModel.prototype, { + toggleDateFilter(searchItemId, generatorId) { + super.toggleDateFilter(searchItemId, generatorId); + const searchItem = this.searchItems[searchItemId]; + if (searchItem.type !== "dateFilter") { + return; + } + const customGenerators = this.query + .filter((el) => el.generatorId?.startsWith("custom")) + .map((el) => el.generatorId); + for (const generatorId of customGenerators) { + this.query = this.query.filter( + (queryElem) => + queryElem.searchItemId !== searchItemId || + !queryElem.generatorId.startsWith("custom") + ); + this.query.push({searchItemId, generatorId}); + const element = getPeriodOptions( + this.referenceMoment, + searchItem.optionsParams + ).find((o) => o.id === generatorId); + const selected_year = this.referenceMoment.plus(element.plusParam).year; + const actual_year = this.referenceMoment.year; + const yearId = getPeriodOptions( + this.referenceMoment, + searchItem.optionsParams + ).find((o) => o.plusParam?.years === selected_year - actual_year).id; + this.query.push({searchItemId, generatorId: yearId}); + } + }, +}); diff --git a/web_time_range_menu_custom/static/src/scss/web_time_range_menu_custom.scss b/web_time_range_menu_custom/static/src/scss/web_time_range_menu_custom.scss new file mode 100644 index 000000000000..02bdb23ee8c9 --- /dev/null +++ b/web_time_range_menu_custom/static/src/scss/web_time_range_menu_custom.scss @@ -0,0 +1,8 @@ +#add_custom_period_wrapper { + padding: 0 20px; + white-space: nowrap; + .d-table-cell { + vertical-align: middle; + padding: 3px 0; + } +} diff --git a/web_time_range_menu_custom/static/src/xml/date_selector.xml b/web_time_range_menu_custom/static/src/xml/date_selector.xml new file mode 100644 index 000000000000..427072a08b1b --- /dev/null +++ b/web_time_range_menu_custom/static/src/xml/date_selector.xml @@ -0,0 +1,74 @@ + + + + + + + diff --git a/web_time_range_menu_custom/static/src/xml/search_bar_menu.xml b/web_time_range_menu_custom/static/src/xml/search_bar_menu.xml new file mode 100644 index 000000000000..ff49a4ea19fd --- /dev/null +++ b/web_time_range_menu_custom/static/src/xml/search_bar_menu.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/web_toggle_chatter/README.rst b/web_toggle_chatter/README.rst new file mode 100644 index 000000000000..03f7339bee74 --- /dev/null +++ b/web_toggle_chatter/README.rst @@ -0,0 +1,88 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================== +Web Toggle Chatter +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:73b8c4d0370aa3eb037f9f93172d5ec9e17859c90da8441457624940566ab11b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :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/18.0/web_toggle_chatter + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_toggle_chatter + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allows to show/hide the chatter in backend form views using a toggle +button. + +|Demo GIF| + +.. |Demo GIF| image:: https://raw.githubusercontent.com/OCA/web/18.0/web_toggle_chatter/static/img/demo.gif + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Vortex Dimensión Digital + +Contributors +------------ + +- `Vortex Dimensión Digital `__: + + - Jorge Rosado Julián + - Juan L. Sánchez + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/web `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_toggle_chatter/__init__.py b/web_toggle_chatter/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/web_toggle_chatter/__manifest__.py b/web_toggle_chatter/__manifest__.py new file mode 100644 index 000000000000..de1c985caea9 --- /dev/null +++ b/web_toggle_chatter/__manifest__.py @@ -0,0 +1,26 @@ +{ + "name": "Web Toggle Chatter", + "summary": "Toggle chatter in backend form views", + "version": "18.0.1.0.0", + "category": "Extra Tools", + "author": "Vortex Dimensión Digital, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/web", + "license": "LGPL-3", + "depends": ["web"], + "assets": { + "web.assets_backend": [ + "web_toggle_chatter/static/src/js/web_toggle_chatter.esm.js", + "web_toggle_chatter/static/src/xml/web_toggle_chatter.xml", + "web_toggle_chatter/static/src/scss/web_toggle_chatter.scss", + ], + "web.assets_unit_tests": [ + "web_toggle_chatter/static/tests/**/*", + ], + }, + "images": [ + "static/description/icon.png", + ], + "installable": True, + "application": False, + "auto_install": False, +} diff --git a/web_toggle_chatter/i18n/web_toggle_chatter.pot b/web_toggle_chatter/i18n/web_toggle_chatter.pot new file mode 100644 index 000000000000..aadee09bfeda --- /dev/null +++ b/web_toggle_chatter/i18n/web_toggle_chatter.pot @@ -0,0 +1,13 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/web_toggle_chatter/pyproject.toml b/web_toggle_chatter/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/web_toggle_chatter/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/web_toggle_chatter/readme/CONTRIBUTORS.md b/web_toggle_chatter/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..a65f81f6574d --- /dev/null +++ b/web_toggle_chatter/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Vortex Dimensión Digital](https://www.dimensionvortex.com/): + - Jorge Rosado Julián \<\> + - Juan L. Sánchez \<\> diff --git a/web_toggle_chatter/readme/DESCRIPTION.md b/web_toggle_chatter/readme/DESCRIPTION.md new file mode 100644 index 000000000000..68a558efaad6 --- /dev/null +++ b/web_toggle_chatter/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +Allows to show/hide the chatter in backend form views using a toggle button. + +![Demo GIF](../static/img/demo.gif) \ No newline at end of file diff --git a/web_toggle_chatter/static/description/icon.png b/web_toggle_chatter/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/web_toggle_chatter/static/description/icon.png differ diff --git a/web_toggle_chatter/static/description/index.html b/web_toggle_chatter/static/description/index.html new file mode 100644 index 000000000000..717b1998e788 --- /dev/null +++ b/web_toggle_chatter/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Web Toggle Chatter

+ +

Beta License: LGPL-3 OCA/web Translate me on Weblate Try me on Runboat

+

Allows to show/hide the chatter in backend form views using a toggle +button.

+

Demo GIF

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Vortex Dimensión Digital
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/web project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/web_toggle_chatter/static/img/demo.gif b/web_toggle_chatter/static/img/demo.gif new file mode 100644 index 000000000000..19f4d018e339 Binary files /dev/null and b/web_toggle_chatter/static/img/demo.gif differ diff --git a/web_toggle_chatter/static/src/js/web_toggle_chatter.esm.js b/web_toggle_chatter/static/src/js/web_toggle_chatter.esm.js new file mode 100644 index 000000000000..3fde2ccd2aff --- /dev/null +++ b/web_toggle_chatter/static/src/js/web_toggle_chatter.esm.js @@ -0,0 +1,292 @@ +import {append, createElement} from "@web/core/utils/xml"; +import {onMounted, onPatched, onWillUnmount, useState} from "@odoo/owl"; +import {FormCompiler} from "@web/views/form/form_compiler"; +import {FormRenderer} from "@web/views/form/form_renderer"; +import {patch} from "@web/core/utils/patch"; + +const SELECTORS = { + chatter: ".o-mail-Form-chatter:not(.o-isInFormSheetBg)", + fallbackChatter: ".o-mail-Form-chatter", + formView: ".o_form_view", + formRenderer: ".o_form_renderer", + rootContent: ".o_content", + sheet: ".o_form_sheet_bg", + toggleButton: ".o_web_toggle_chatter_toggle_btn", + toggleWrapper: ".o_web_toggle_chatter_toggle_wrapper", +}; + +const CLASSES = { + collapsed: "o_web_toggle_chatter_collapsed", + enabled: "o_web_toggle_chatter_enabled", + flexColumn: "flex-column", + mobileMode: "o_web_toggle_chatter_mobile_mode", + toggleWrapper: "o_web_toggle_chatter_toggle_wrapper", +}; + +const TEMPLATES = { + toggleButton: "web_toggle_chatter.ChatterToggleButton", +}; + +const WIDTHS = { + chatter: "30%", + sheetExpanded: "70%", + sheetCollapsed: "100%", +}; + +const INLINE_STYLE_PROPERTIES = [ + "max-width", + "flex-basis", + "opacity", + "pointer-events", +]; + +patch(FormCompiler.prototype, { + compile(node, params = {}) { + const compiledArch = super.compile(node, params); + if (params.isSubView) { + return compiledArch; + } + this._ensureChatterToggleButton(compiledArch); + return compiledArch; + }, + + _ensureChatterToggleButton(compiledArch) { + if (compiledArch.querySelector(`.${CLASSES.toggleWrapper}`)) { + return; + } + const chatterContainer = + compiledArch.querySelector(SELECTORS.chatter) || + compiledArch.querySelector(SELECTORS.fallbackChatter); + if (!chatterContainer || !chatterContainer.parentNode) { + return; + } + const formView = + chatterContainer.closest(SELECTORS.formView) || + compiledArch.querySelector(SELECTORS.formView); + formView?.classList.add(CLASSES.enabled); + chatterContainer.parentNode.insertBefore( + this._createChatterToggleWrapper(), + chatterContainer + ); + }, + + _createChatterToggleWrapper() { + const toggleWrapper = createElement("div"); + toggleWrapper.classList.add(CLASSES.toggleWrapper); + append(toggleWrapper, this._createChatterToggleButtonTemplateNode()); + return toggleWrapper; + }, + + _createChatterToggleButtonTemplateNode() { + const templateNode = createElement("t"); + templateNode.setAttribute("t-call", TEMPLATES.toggleButton); + return templateNode; + }, +}); + +patch(FormRenderer.prototype, { + setup() { + super.setup(); + this.webToggleChatterState = useState({ + isVisible: true, + }); + this._onViewportChange = this._onViewportChange.bind(this); + this.onToggleChatter = this.onToggleChatter.bind(this); + onMounted(() => { + window.addEventListener("resize", this._onViewportChange); + this._syncChatterLayout(); + }); + onPatched(() => this._syncChatterLayout()); + onWillUnmount(() => { + window.removeEventListener("resize", this._onViewportChange); + }); + }, + + onToggleChatter(event) { + const formContainer = this._resolveFormContainer(event); + if (formContainer && this._isMobileLayout(formContainer)) { + this._syncChatterLayout(event); + return; + } + this.webToggleChatterState.isVisible = !this.webToggleChatterState.isVisible; + this._syncChatterLayout(event); + }, + + _syncChatterLayout(event = null) { + const formContainer = this._resolveFormContainer(event); + if (!formContainer) { + return; + } + const chatterContainer = this._resolveChatterContainer(formContainer); + if (!chatterContainer) { + formContainer.classList.remove(CLASSES.enabled); + formContainer.classList.remove(CLASSES.collapsed); + formContainer.classList.remove(CLASSES.mobileMode); + return; + } + this._ensureRuntimeToggleWrapper(formContainer, chatterContainer); + if (!formContainer.querySelector(SELECTORS.toggleWrapper)) { + formContainer.classList.remove(CLASSES.enabled); + return; + } + formContainer.classList.add(CLASSES.enabled); + this._syncToggleButtonIcon(formContainer); + const isMobileLayout = this._isMobileLayout(formContainer); + formContainer.classList.toggle(CLASSES.mobileMode, isMobileLayout); + if (isMobileLayout) { + formContainer.classList.remove(CLASSES.collapsed); + this._clearInlineStyles(formContainer, chatterContainer); + return; + } + const isCollapsed = !this.webToggleChatterState.isVisible; + + formContainer.classList.toggle(CLASSES.collapsed, isCollapsed); + this._applyDesktopInlineStyles(formContainer, chatterContainer, isCollapsed); + }, + + _applyDesktopInlineStyles(formContainer, chatterContainer, isCollapsed) { + const sheetContainer = formContainer.querySelector(SELECTORS.sheet); + chatterContainer.style.maxWidth = isCollapsed ? "0" : WIDTHS.chatter; + chatterContainer.style.flexBasis = isCollapsed ? "0" : WIDTHS.chatter; + chatterContainer.style.opacity = isCollapsed ? "0" : "1"; + if (isCollapsed) { + chatterContainer.style.pointerEvents = "none"; + if (sheetContainer) { + sheetContainer.style.width = WIDTHS.sheetCollapsed; + } + return; + } + chatterContainer.style.removeProperty("pointer-events"); + if (sheetContainer) { + sheetContainer.style.width = WIDTHS.sheetExpanded; + } + }, + + _clearInlineStyles(formContainer, chatterContainer) { + const sheetContainer = formContainer.querySelector(SELECTORS.sheet); + for (const propertyName of INLINE_STYLE_PROPERTIES) { + chatterContainer.style.removeProperty(propertyName); + } + if (sheetContainer) { + sheetContainer.style.removeProperty("width"); + } + }, + + _ensureRuntimeToggleWrapper(formContainer, chatterContainer) { + if (formContainer.querySelector(SELECTORS.toggleWrapper)) { + return; + } + const toggleWrapper = document.createElement("div"); + toggleWrapper.classList.add(CLASSES.toggleWrapper); + const toggleButton = document.createElement("button"); + toggleButton.type = "button"; + toggleButton.className = "o_web_toggle_chatter_toggle_btn btn btn-light"; + toggleButton.setAttribute("aria-label", "Toggle chatter"); + const icon = document.createElement("i"); + icon.className = "fa fa-angle-right"; + toggleButton.appendChild(icon); + toggleButton.addEventListener("click", (clickEvent) => { + clickEvent.preventDefault(); + this.onToggleChatter(clickEvent); + }); + toggleWrapper.appendChild(toggleButton); + chatterContainer.parentNode.insertBefore(toggleWrapper, chatterContainer); + }, + + _syncToggleButtonIcon(formContainer) { + const toggleButton = formContainer.querySelector(SELECTORS.toggleButton); + if (!toggleButton) { + return; + } + const icon = toggleButton.querySelector(".fa"); + const isVisible = this.webToggleChatterState.isVisible; + toggleButton.setAttribute( + "aria-label", + isVisible ? "Toggle chatter" : "Show chatter" + ); + if (!icon) { + return; + } + icon.classList.toggle("fa-angle-right", isVisible); + icon.classList.toggle("fa-angle-left", !isVisible); + }, + + _onViewportChange() { + this._syncChatterLayout(); + }, + + _isMobileLayout(formContainer) { + const layoutContainer = this._resolveLayoutContainer(formContainer); + if (!layoutContainer) { + return false; + } + if (layoutContainer.classList.contains(CLASSES.flexColumn)) { + return true; + } + return window.getComputedStyle(layoutContainer).flexDirection === "column"; + }, + + _resolveLayoutContainer(formContainer) { + if (!formContainer) { + return null; + } + if (formContainer.matches(SELECTORS.formRenderer)) { + return formContainer; + } + return ( + formContainer.closest(SELECTORS.formRenderer) || + formContainer.querySelector(SELECTORS.formRenderer) || + formContainer + ); + }, + + _resolveFormContainer(event = null) { + const candidates = [event?.currentTarget, event?.target, this.el]; + for (const candidate of candidates) { + const formContainer = this._closestFormContainer(candidate); + if (formContainer) { + return this._normalizeFormContainer(formContainer); + } + } + const rootFormView = document.querySelector( + `${SELECTORS.rootContent} ${SELECTORS.formView}` + ); + if (rootFormView) { + return rootFormView; + } + return ( + this._normalizeFormContainer( + document.querySelector( + `${SELECTORS.rootContent} ${SELECTORS.formRenderer}` + ) + ) || null + ); + }, + + _closestFormContainer(element) { + if (!element || typeof element.closest !== "function") { + return null; + } + return ( + element.closest(SELECTORS.formView) || + element.closest(SELECTORS.formRenderer) + ); + }, + + _normalizeFormContainer(formContainer) { + if (!formContainer) { + return null; + } + if (formContainer.matches(SELECTORS.formRenderer)) { + return formContainer.querySelector(SELECTORS.formView) || formContainer; + } + return formContainer; + }, + + _resolveChatterContainer(formContainer) { + return ( + formContainer.querySelector(SELECTORS.chatter) || + formContainer.querySelector(SELECTORS.fallbackChatter) + ); + }, +}); diff --git a/web_toggle_chatter/static/src/scss/web_toggle_chatter.scss b/web_toggle_chatter/static/src/scss/web_toggle_chatter.scss new file mode 100644 index 000000000000..ba5c3c9e5134 --- /dev/null +++ b/web_toggle_chatter/static/src/scss/web_toggle_chatter.scss @@ -0,0 +1,127 @@ +.o_form_view.o_web_toggle_chatter_enabled:not(.o_web_toggle_chatter_mobile_mode), +.o_form_renderer.o_web_toggle_chatter_enabled:not(.o_web_toggle_chatter_mobile_mode) { + --o-web-toggle-chatter-toggle-size: 34px; + --o-web-toggle-chatter-toggle-offset: -34px; + --o-web-toggle-chatter-width: 30%; + --o-web-toggle-chatter-sheet-width: 70%; + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + .o_web_toggle_chatter_toggle_wrapper { + position: relative; + width: 0; + min-width: 0; + z-index: 10; + } + + .o_web_toggle_chatter_toggle_btn { + position: absolute; + left: var(--o-web-toggle-chatter-toggle-offset); + top: 50%; + transform: translateY(-50%); + z-index: 100; + width: var(--o-web-toggle-chatter-toggle-size); + height: var(--o-web-toggle-chatter-toggle-size); + padding: 0; + box-sizing: border-box; + border: 1px solid var(--border-color, #d8dadd); + border-radius: 50%; + background: var(--o-view-background-color, #fff); + display: flex; + align-items: center; + justify-content: center; + color: var(--o-main-text-color, #495057); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + transition: + background-color 0.2s ease, + border-color 0.2s ease, + color 0.2s ease; + + &:hover { + background: var(--bs-gray-100, #f3f5f7); + border-color: var(--bs-gray-400, #c6ccd2); + color: var(--o-gray-900, #212529); + } + + &:focus { + box-shadow: 0 0 0 2px rgba(113, 125, 136, 0.25); + } + + .fa { + font-size: 22px; + line-height: 1; + display: block; + } + + .fa-angle-right { + transform: translate(1px, -1px); + } + + .fa-angle-left { + transform: translate(-1px, -1px); + } + } + + .o-mail-Form-chatter { + flex: 0 0 var(--o-web-toggle-chatter-width); + max-width: var(--o-web-toggle-chatter-width); + min-width: 0; + overflow: hidden; + opacity: 1; + transition: + flex-basis 0.25s ease, + max-width 0.25s ease, + opacity 0.2s ease; + } + + .o_form_sheet_bg { + flex: 1 1 auto; + width: var(--o-web-toggle-chatter-sheet-width); + max-width: 100%; + min-width: 0; + transition: width 0.3s ease; + } + + &.o_web_toggle_chatter_collapsed { + overflow-x: hidden; + + .o_form_sheet_bg { + width: 100%; + } + + .o-mail-Form-chatter { + flex-basis: 0; + max-width: 0; + opacity: 0; + pointer-events: none; + } + } +} + +.o_form_view.o_web_toggle_chatter_enabled.o_web_toggle_chatter_mobile_mode, +.o_form_renderer.o_web_toggle_chatter_enabled.o_web_toggle_chatter_mobile_mode { + .o_web_toggle_chatter_toggle_wrapper { + display: none; + } +} + +.o_form_view.o_web_toggle_chatter_enabled.flex-column, +.o_form_renderer.o_web_toggle_chatter_enabled.flex-column, +.o_form_view.o_web_toggle_chatter_enabled .o_form_renderer.flex-column { + .o_web_toggle_chatter_toggle_wrapper { + display: none; + } + + .o-mail-Form-chatter { + flex-basis: auto; + max-width: none; + opacity: 1; + pointer-events: auto; + } + + .o_form_sheet_bg { + width: auto; + } +} diff --git a/web_toggle_chatter/static/src/xml/web_toggle_chatter.xml b/web_toggle_chatter/static/src/xml/web_toggle_chatter.xml new file mode 100644 index 000000000000..225732dabcd9 --- /dev/null +++ b/web_toggle_chatter/static/src/xml/web_toggle_chatter.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/web_toggle_chatter/static/tests/web_toggle_chatter.test.js b/web_toggle_chatter/static/tests/web_toggle_chatter.test.js new file mode 100644 index 000000000000..9b4af4d17fec --- /dev/null +++ b/web_toggle_chatter/static/tests/web_toggle_chatter.test.js @@ -0,0 +1,151 @@ +import {expect, test} from "@odoo/hoot"; +import { + contains, + defineModels, + fields, + models, + mountView, + patchWithCleanup, +} from "@web/../tests/web_test_helpers"; +import {FormCompiler} from "@web/views/form/form_compiler"; + +class Partner extends models.Model { + name = fields.Char(); + _records = [{id: 1, name: "Test Partner"}]; +} + +defineModels([Partner]); + +/** + * Patches FormCompiler to inject a mock chatter element into the compiled arch. + * This allows testing the toggle behavior without requiring the mail module. + */ +function patchFormCompilerWithMockChatter() { + patchWithCleanup(FormCompiler.prototype, { + compile(node, params = {}) { + const compiledArch = super.compile(node, params); + if (!params.isSubView) { + const formViewEl = compiledArch.querySelector(".o_form_view"); + if (formViewEl) { + const mockChatter = document.createElement("div"); + mockChatter.className = "o-mail-Form-chatter"; + formViewEl.appendChild(mockChatter); + } + } + return compiledArch; + }, + }); +} + +const FORM_ARCH = ` +
+ + + +
+`; + +test("Toggle button is not injected in form view without chatter", async () => { + await mountView({ + resModel: "partner", + type: "form", + arch: FORM_ARCH, + resId: 1, + }); + expect(".o_web_toggle_chatter_toggle_wrapper").toHaveCount(0); + expect(".o_web_toggle_chatter_toggle_btn").toHaveCount(0); +}); + +test("Toggle button is injected when chatter is present", async () => { + patchFormCompilerWithMockChatter(); + await mountView({ + resModel: "partner", + type: "form", + arch: FORM_ARCH, + resId: 1, + }); + expect(".o_web_toggle_chatter_toggle_wrapper").toHaveCount(1); + expect(".o_web_toggle_chatter_toggle_btn").toHaveCount(1); +}); + +test("Clicking toggle collapses the chatter", async () => { + patchFormCompilerWithMockChatter(); + await mountView({ + resModel: "partner", + type: "form", + arch: FORM_ARCH, + resId: 1, + }); + // Initially visible - no collapsed class + expect(".o_form_view").not.toHaveClass("o_web_toggle_chatter_collapsed"); + + await contains(".o_web_toggle_chatter_toggle_btn").click(); + + // After toggle: form gets collapsed class, chatter loses visibility + expect(".o_form_view").toHaveClass("o_web_toggle_chatter_collapsed"); + expect(".o-mail-Form-chatter").toHaveStyle({opacity: "0"}); + expect(".o-mail-Form-chatter").toHaveStyle({maxWidth: "0"}); +}); + +test("Clicking toggle twice restores the chatter", async () => { + patchFormCompilerWithMockChatter(); + await mountView({ + resModel: "partner", + type: "form", + arch: FORM_ARCH, + resId: 1, + }); + await contains(".o_web_toggle_chatter_toggle_btn").click(); + expect(".o_form_view").toHaveClass("o_web_toggle_chatter_collapsed"); + + await contains(".o_web_toggle_chatter_toggle_btn").click(); + + expect(".o_form_view").not.toHaveClass("o_web_toggle_chatter_collapsed"); + expect(".o-mail-Form-chatter").toHaveStyle({opacity: "1"}); +}); + +test("Toggle does not apply custom chatter styles in mobile layout", async () => { + patchFormCompilerWithMockChatter(); + await mountView({ + resModel: "partner", + type: "form", + arch: FORM_ARCH, + resId: 1, + }); + const formView = document.querySelector(".o_form_view"); + const formRenderer = document.querySelector(".o_form_renderer") || formView; + formRenderer.classList.add("flex-column"); + + await contains(".o_web_toggle_chatter_toggle_btn").click(); + + expect(".o_form_view").not.toHaveClass("o_web_toggle_chatter_collapsed"); + expect(".o_form_view").toHaveClass("o_web_toggle_chatter_mobile_mode"); + expect(".o-mail-Form-chatter").not.toHaveStyle({maxWidth: "0"}); + expect(".o-mail-Form-chatter").not.toHaveStyle({flexBasis: "0"}); + expect(".o-mail-Form-chatter").not.toHaveStyle({opacity: "0"}); +}); + +test("Switching from desktop to mobile clears collapsed inline chatter styles", async () => { + patchFormCompilerWithMockChatter(); + await mountView({ + resModel: "partner", + type: "form", + arch: FORM_ARCH, + resId: 1, + }); + + await contains(".o_web_toggle_chatter_toggle_btn").click(); + expect(".o_form_view").toHaveClass("o_web_toggle_chatter_collapsed"); + expect(".o-mail-Form-chatter").toHaveStyle({maxWidth: "0"}); + + const formView = document.querySelector(".o_form_view"); + const formRenderer = document.querySelector(".o_form_renderer") || formView; + formRenderer.classList.add("flex-column"); + window.dispatchEvent(new Event("resize")); + + expect(".o_form_view").toHaveClass("o_web_toggle_chatter_mobile_mode"); + expect(".o_form_view").not.toHaveClass("o_web_toggle_chatter_collapsed"); + expect(".o-mail-Form-chatter").not.toHaveStyle({maxWidth: "0"}); + expect(".o-mail-Form-chatter").not.toHaveStyle({flexBasis: "0"}); + expect(".o-mail-Form-chatter").not.toHaveStyle({opacity: "0"}); +});