From bf720d7e7c07d2a38487271c6e115ebee48e1d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Thu, 23 Apr 2026 03:46:58 +0000 Subject: [PATCH] [ADD] web_list_record_popup: new module --- web_list_record_popup/README.rst | 81 ++++ web_list_record_popup/__init__.py | 1 + web_list_record_popup/__manifest__.py | 18 + web_list_record_popup/models/__init__.py | 1 + .../models/web_list_record_popup_mixin.py | 66 +++ web_list_record_popup/pyproject.toml | 3 + web_list_record_popup/readme/CONTRIBUTORS.md | 1 + web_list_record_popup/readme/DESCRIPTION.md | 6 + web_list_record_popup/readme/README.rst | 74 +++ .../static/description/index.html | 429 ++++++++++++++++++ .../src/js/list_renderer_with_button.esm.js | 73 +++ .../tests/list_renderer_with_button.test.js | 105 +++++ 12 files changed, 858 insertions(+) create mode 100644 web_list_record_popup/README.rst create mode 100644 web_list_record_popup/__init__.py create mode 100644 web_list_record_popup/__manifest__.py create mode 100644 web_list_record_popup/models/__init__.py create mode 100644 web_list_record_popup/models/web_list_record_popup_mixin.py create mode 100644 web_list_record_popup/pyproject.toml create mode 100644 web_list_record_popup/readme/CONTRIBUTORS.md create mode 100644 web_list_record_popup/readme/DESCRIPTION.md create mode 100644 web_list_record_popup/readme/README.rst create mode 100644 web_list_record_popup/static/description/index.html create mode 100644 web_list_record_popup/static/src/js/list_renderer_with_button.esm.js create mode 100644 web_list_record_popup/static/tests/list_renderer_with_button.test.js diff --git a/web_list_record_popup/README.rst b/web_list_record_popup/README.rst new file mode 100644 index 000000000000..0b0bc601fc8b --- /dev/null +++ b/web_list_record_popup/README.rst @@ -0,0 +1,81 @@ +===================== +Web List Record Popup +===================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:98d747783d9eeaa78fab65334d871778a70c865edf1d26e076b9dbf3d179b1c5 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/licence-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_list_record_popup + :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_list_record_popup + :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| + +**Features:** + +- Adds a popup button to editable lists in form views +- Opens records in a form dialog instead of inline editing +- Works with both existing and new (NewID) records +- Optional activation via mixin - no hard dependencies + +**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 +------- + +* Akretion + +Contributors +------------ + +- Raphael Valyi + +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_list_record_popup/__init__.py b/web_list_record_popup/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/web_list_record_popup/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/web_list_record_popup/__manifest__.py b/web_list_record_popup/__manifest__.py new file mode 100644 index 000000000000..87896d40fee7 --- /dev/null +++ b/web_list_record_popup/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "Web List Record Popup", + "version": "18.0.1.0.0", + "category": "Hidden", + "license": "LGPL-3", + "author": "Akretion, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/web", + "depends": ["web"], + "data": [], + "assets": { + "web.assets_backend": [ + "web_list_record_popup/static/src/js/list_renderer_with_button.esm.js", + ], + "web.assets_unit_tests": [ + "web_list_record_popup/static/tests/list_renderer_with_button.test.js", + ], + }, +} diff --git a/web_list_record_popup/models/__init__.py b/web_list_record_popup/models/__init__.py new file mode 100644 index 000000000000..3cc1ae6f61e9 --- /dev/null +++ b/web_list_record_popup/models/__init__.py @@ -0,0 +1 @@ +from . import web_list_record_popup_mixin diff --git a/web_list_record_popup/models/web_list_record_popup_mixin.py b/web_list_record_popup/models/web_list_record_popup_mixin.py new file mode 100644 index 000000000000..0e699274856a --- /dev/null +++ b/web_list_record_popup/models/web_list_record_popup_mixin.py @@ -0,0 +1,66 @@ +# Copyright 2026-TODAY Akretion - Raphael Valyi +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +from lxml import etree + +from odoo import api, models + + +class WebListRecordPopupMixin(models.AbstractModel): + """Mixin to inject popup button into form views. + + Models inheriting from this mixin can define: + - _popup_button_xpaths: List of tuples (xpath, position) where to inject the button + + Example: + _popup_button_xpaths = [ + ( + "//field[@name='invoice_line_ids']/list/field[@name='product_id']", + "before" + ), + ] + """ + + _name = "web_list_record_popup.mixin" + _description = "Mixin to inject popup button into form views" + + _popup_button_xpaths = [] + + @api.model + def _get_view(self, view_id=None, view_type="form", **options): + arch, view = super()._get_view(view_id, view_type, **options) + + if view_type == "form" and self._popup_button_xpaths: + arch = self._inject_popup_buttons(arch) + + return arch, view + + def _inject_popup_buttons(self, arch): + """Inject popup button before specified fields in list views inside forms.""" + # Check if web_list_record_popup module is installed + if not self.env["ir.module.module"].search( + [("name", "=", "web_list_record_popup"), ("state", "=", "installed")] + ): + return arch + + for xpath_expr, position in self._popup_button_xpaths: + try: + nodes = arch.findall(xpath_expr) + for node in nodes: + button = etree.Element("button") + button.set("name", "dummy_button_for_js") + button.set("icon", "fa-external-link") + button.set("title", "Edit in Form") + button.set("class", "btn-sm btn-link p-0 edit-line-popup") + + if position == "before": + node.addprevious(button) + elif position == "after": + node.addnext(button) + elif position == "inside": + node.insert(0, button) + except Exception: + # If xpath fails, skip this injection + continue + + return arch diff --git a/web_list_record_popup/pyproject.toml b/web_list_record_popup/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/web_list_record_popup/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/web_list_record_popup/readme/CONTRIBUTORS.md b/web_list_record_popup/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..3beded6a6d76 --- /dev/null +++ b/web_list_record_popup/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Raphael Valyi \<\> diff --git a/web_list_record_popup/readme/DESCRIPTION.md b/web_list_record_popup/readme/DESCRIPTION.md new file mode 100644 index 000000000000..d8217c0db401 --- /dev/null +++ b/web_list_record_popup/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +**Features:** + +- Adds a popup button to editable lists in form views +- Opens records in a form dialog instead of inline editing +- Works with both existing and new (NewID) records +- Optional activation via mixin - no hard dependencies diff --git a/web_list_record_popup/readme/README.rst b/web_list_record_popup/readme/README.rst new file mode 100644 index 000000000000..6b154c7b5090 --- /dev/null +++ b/web_list_record_popup/readme/README.rst @@ -0,0 +1,74 @@ +Web List Record Popup +===================== + +This module provides a popup button for editable lists in Odoo forms. +When clicked, it opens the record in a form dialog instead of editing inline. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +This module provides a mixin that can be inherited by any model to add popup buttons +to x2many list fields in form views. + +To use it: + +1. Make your model inherit from ``web_list_record_popup.mixin`` +2. Define ``_popup_button_xpaths`` class attribute with xpath expressions where buttons should be injected + +Example:: + + class MyModel(models.Model): + _inherit = ["web_list_record_popup.mixin"] + + _popup_button_xpaths = [ + ("//field[@name='line_ids']/list/field[@name='product_id']", "before"), + ] + +The button will only be injected if the ``web_list_record_popup`` module is installed. + +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 +~~~~~~~ + +* Akretion +* Raphael Valyi +* Odoo Community Association (OCA) + +Contributors +~~~~~~~~~~~~ + +* Raphael Valyi + +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_list_record_popup/static/description/index.html b/web_list_record_popup/static/description/index.html new file mode 100644 index 000000000000..f7806e611419 --- /dev/null +++ b/web_list_record_popup/static/description/index.html @@ -0,0 +1,429 @@ + + + + + +Web List Record Popup + + + +
+

Web List Record Popup

+ + +

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

+

Features:

+
    +
  • Adds a popup button to editable lists in form views
  • +
  • Opens records in a form dialog instead of inline editing
  • +
  • Works with both existing and new (NewID) records
  • +
  • Optional activation via mixin - no hard dependencies
  • +
+

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

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

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_list_record_popup/static/src/js/list_renderer_with_button.esm.js b/web_list_record_popup/static/src/js/list_renderer_with_button.esm.js new file mode 100644 index 000000000000..51a5c152088d --- /dev/null +++ b/web_list_record_popup/static/src/js/list_renderer_with_button.esm.js @@ -0,0 +1,73 @@ +/* @odoo-module */ +/* eslint-disable sort-imports */ +// Copyright 2026-TODAY Akretion - Raphael Valyi +// License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + +import {patch} from "@web/core/utils/patch"; +import {ViewButton} from "@web/views/view_button/view_button"; +import {ListRenderer} from "@web/views/list/list_renderer"; +import {X2ManyField} from "@web/views/fields/x2many/x2many_field"; +import {useBus} from "@web/core/utils/hooks"; +import {EventBus} from "@odoo/owl"; + +// Create a dedicated bus for this module +export const listPopupBus = new EventBus(); + +// 1. Intercept the custom popup button click +patch(ViewButton.prototype, { + onClick(ev) { + const isPopupButton = + (this.clickParams && this.clickParams.name === "dummy_button_for_js") || + (this.props.className && this.props.className.includes("edit-line-popup")); + + if (isPopupButton) { + ev.preventDefault(); + ev.stopPropagation(); + if (this.props.record) { + listPopupBus.trigger("OPEN_LINE_IN_POPUP", {record: this.props.record}); + } + return; + } + return super.onClick(ev); + }, +}); + +// 2. Catch the event and open the record in dialog +patch(ListRenderer.prototype, { + setup() { + super.setup(); + useBus(listPopupBus, "OPEN_LINE_IN_POPUP", (ev) => { + const payload = ev.detail || ev; + const recordId = payload?.record?.id; + + if (recordId) { + // Find the record in the list by ID (safer than reference comparison) + const localRecord = this.props.list.records.find( + (r) => r.id === recordId + ); + if (localRecord) { + // Leave edit mode first to ensure clean state + this.props.list.leaveEditMode().then(() => { + // Call openRecord which will be handled by X2ManyField + if (this.props.openRecord) { + this.props.openRecord(localRecord); + } + }); + } + } + }); + }, +}); + +// 3. Override X2ManyField to allow opening records even when editable +patch(X2ManyField.prototype, { + async openRecord(record) { + // Always allow opening the record, bypassing canOpenRecord check + // This is triggered by our custom button + return this._openRecord({ + record, + context: this.props.context, + mode: this.props.readonly ? "readonly" : "edit", + }); + }, +}); diff --git a/web_list_record_popup/static/tests/list_renderer_with_button.test.js b/web_list_record_popup/static/tests/list_renderer_with_button.test.js new file mode 100644 index 000000000000..43a421caf336 --- /dev/null +++ b/web_list_record_popup/static/tests/list_renderer_with_button.test.js @@ -0,0 +1,105 @@ +/* @odoo-module */ +/* eslint-disable sort-imports */ + +import {animationFrame} from "@odoo/hoot-mock"; +import {click} from "@odoo/hoot-dom"; +import {expect, test} from "@odoo/hoot"; +import {defineModels, fields, models, mountView} from "@web/../tests/web_test_helpers"; + +class Parent extends models.Model { + name = fields.Char(); + line_ids = fields.One2many({ + string: "Lines", + relation: "line", + relation_field: "parent_id", + }); + + _records = [ + { + id: 1, + name: "Test Parent Record", + line_ids: [1], + }, + ]; +} + +class Line extends models.Model { + name = fields.Char(); + parent_id = fields.Many2one({relation: "parent"}); + + _records = [ + { + id: 1, + name: "Test Line Record 1", + parent_id: 1, + }, + ]; +} + +defineModels([Parent, Line]); + +test("click on edit-line-popup button inside a list opens the form dialog", async () => { + await mountView({ + type: "form", + resModel: "parent", + resId: 1, + arch: ` +
+ + + +