diff --git a/document_page_reference/README.rst b/document_page_reference/README.rst index 53b9493b62a..2d259d2844a 100644 --- a/document_page_reference/README.rst +++ b/document_page_reference/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ======================= Document Page Reference ======================= @@ -17,7 +13,7 @@ Document Page Reference .. |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 +.. |badge2| image:: https://img.shields.io/badge/licence-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%2Fknowledge-lightgray.png?logo=github diff --git a/document_page_reference/__manifest__.py b/document_page_reference/__manifest__.py index dea07a92925..0d13672558f 100644 --- a/document_page_reference/__manifest__.py +++ b/document_page_reference/__manifest__.py @@ -18,6 +18,9 @@ "web.assets_backend": [ "document_page_reference/static/src/js/**/*", ], + "web.assets_tests": [ + "document_page_reference/static/tests/**/*", + ], }, "maintainers": ["etobella"], } diff --git a/document_page_reference/models/document_page.py b/document_page_reference/models/document_page.py index 5c5fd37e267..aeee8e71e07 100644 --- a/document_page_reference/models/document_page.py +++ b/document_page_reference/models/document_page.py @@ -3,6 +3,8 @@ import logging +from markupsafe import Markup + from odoo import _, api, fields, models, tools from odoo.exceptions import ValidationError from odoo.tools.misc import html_escape @@ -52,7 +54,32 @@ class DocumentPage(models.Model): reference = fields.Char( help="Used to find the document, it can contain letters, numbers and _" ) - content_parsed = fields.Html(compute="_compute_content_parsed") + content_parsed = fields.Html(compute="_compute_content_parsed", sanitize=False) + + def _get_page_index(self, link=True): + """Override to use oe_direct_line links compatible with the widget.""" + self.ensure_one() + index = [ + Markup("
  • ") + subpage._get_page_index() + Markup("
  • ") + for subpage in self.child_ids + ] + r = Markup("") + if link: + r = ( + Markup( + '' + ) + % ( + self._name, + self.id, + ) + + html_escape(self.name) + + Markup("") + ) + if index: + r += Markup("") + return r def get_formview_action(self, access_uid=None): res = super().get_formview_action(access_uid) @@ -60,17 +87,20 @@ def get_formview_action(self, access_uid=None): res["views"] = [(view_id, "form")] return res - @api.depends("history_head") + @api.depends("history_head", "type") def _compute_content_parsed(self): for record in self: - content = record.get_content() - if content == "

    " and self.content != "

    ": - _logger.error( - "Template from page with id = %s cannot be processed correctly" - % self.id - ) - content = self.content - record.content_parsed = content + if record.type == "category": + record.content_parsed = record.content + else: + content = record.get_content() + if content == "

    " and record.content != "

    ": + _logger.error( + "Template from page with id = %s cannot be " + "processed correctly" % record.id + ) + content = record.content + record.content_parsed = content @api.constrains("reference") def _check_reference(self): diff --git a/document_page_reference/static/description/index.html b/document_page_reference/static/description/index.html index dfb52bcbef2..568ef4db19e 100644 --- a/document_page_reference/static/description/index.html +++ b/document_page_reference/static/description/index.html @@ -3,16 +3,15 @@ -README.rst +Document Page Reference -

    +
    +

    Document Page Reference

    - - -Odoo Community Association - -
    -

    Document Page Reference

    -

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

    +

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

    This module allows to add a reference name on documents and simplifies the link between document pages.

    Table of contents

    @@ -391,13 +385,13 @@

    Document Page Reference

    -

    Usage

    +

    Usage

    When editing a document page add elements like ${XXX} where XXX is the reference of another page. Now, when viewing the document, it will link directly to the page. Also, the name will be parsed as the display name.

    -

    Bug Tracker

    +

    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 @@ -405,25 +399,23 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Creu Blanca
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    - -Odoo Community Association - +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.

    @@ -434,6 +426,5 @@

    Maintainers

    -
    diff --git a/document_page_reference/static/src/js/editor.esm.js b/document_page_reference/static/src/js/editor.esm.js new file mode 100644 index 00000000000..98fbe7f3654 --- /dev/null +++ b/document_page_reference/static/src/js/editor.esm.js @@ -0,0 +1,42 @@ +/* @odoo-module */ + +import {HtmlField} from "@web_editor/js/backend/html_field"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; +import {onMounted, onPatched} from "@odoo/owl"; + +export class DocumentPageReferenceField extends HtmlField { + setup() { + super.setup(); + this.actionService = useService("action"); + this.orm = useService("orm"); + this._onClickDirectLink = this._onClickDirectLink.bind(this); + onMounted(() => this._bindLinks()); + onPatched(() => this._bindLinks()); + } + _bindLinks() { + const el = this.readonlyElementRef && this.readonlyElementRef.el; + if (!el) return; + // Remove target="_blank" from internal reference links + // (added by retargetLinks in HtmlField) + for (const link of el.querySelectorAll("a.oe_direct_line")) { + link.removeAttribute("target"); + link.removeAttribute("rel"); + link.removeEventListener("click", this._onClickDirectLink); + link.addEventListener("click", this._onClickDirectLink); + } + } + _onClickDirectLink(ev) { + ev.preventDefault(); + ev.stopPropagation(); + const target = ev.currentTarget; + const model = target.dataset.oeModel; + const id = parseInt(target.dataset.oeId, 10); + if (!model || !id) return; + this.orm.call(model, "get_formview_action", [[id]]).then((action) => { + this.actionService.doAction(action); + }); + } +} + +registry.category("fields").add("document_page_reference", DocumentPageReferenceField); diff --git a/document_page_reference/static/src/js/editor.js b/document_page_reference/static/src/js/editor.js deleted file mode 100644 index 852df084ca9..00000000000 --- a/document_page_reference/static/src/js/editor.js +++ /dev/null @@ -1,34 +0,0 @@ -odoo.define("document_page_reference.backend", function (require) { - "use strict"; - - var field_registry = require("web.field_registry"); - var FieldTextHtmlSimple = require("web_editor.field.html"); - var FieldDocumentPage = FieldTextHtmlSimple.extend({ - events: _.extend({}, FieldTextHtmlSimple.prototype.events, { - "click .oe_direct_line": "_onClickDirectLink", - }), - _onClickDirectLink: function (event) { - var self = this; - event.preventDefault(); - event.stopPropagation(); - var element = $(event.target).closest(".oe_direct_line")[0]; - var default_reference = element.name; - var model = $(event.target).data("oe-model"); - var id = $(event.target).data("oe-id"); - var context = this.record.getContext(this.recordParams); - if (default_reference) { - context.default_reference = default_reference; - } - this._rpc({ - model: model, - method: "get_formview_action", - args: [[parseInt(id, 10)]], - context: context, - }).then(function (action) { - self.trigger_up("do_action", {action: action}); - }); - }, - }); - field_registry.add("document_page_reference", FieldDocumentPage); - return FieldDocumentPage; -}); diff --git a/document_page_reference/static/tests/test_document_page_reference_tour.esm.js b/document_page_reference/static/tests/test_document_page_reference_tour.esm.js new file mode 100644 index 00000000000..ab44e7b88ca --- /dev/null +++ b/document_page_reference/static/tests/test_document_page_reference_tour.esm.js @@ -0,0 +1,84 @@ +/** @odoo-module */ + +import tour from "web_tour.tour"; + +/* + * Test 1: Reference widget renders ${ref} as clickable links. + */ +tour.register( + "document_page_reference_widget_tour", + { + test: true, + url: "/web#action=document_page.action_page", + }, + [ + { + content: "Open Test Ref Page 1", + trigger: '.o_data_cell[name="name"]:contains("Test Ref Page 1")', + run: "click", + }, + { + content: "Verify content_parsed renders reference as link", + trigger: + '.o_form_view .o_field_widget[name="content_parsed"] a.oe_direct_line', + timeout: 20000, + run: function () { + var link = this.$anchor[0]; + if (!link.dataset.oeModel || !link.dataset.oeId) { + throw new Error("Reference link missing data-oe-model/data-oe-id"); + } + if (link.getAttribute("target") === "_blank") { + throw new Error( + "Internal reference link should not have target=_blank" + ); + } + }, + }, + ] +); + +/* + * Test 2: Category page index uses oe_direct_line links. + */ +tour.register( + "document_page_reference_category_tour", + { + test: true, + url: "/web#action=document_page.action_page", + }, + [ + { + content: "Open Test Ref Page 1 to navigate to its category", + trigger: '.o_data_cell[name="name"]:contains("Test Ref Page 1")', + run: "click", + }, + { + content: "Navigate to category via parent_id link", + trigger: '.o_form_view .o_field_widget[name="parent_id"] a', + timeout: 20000, + run: "click", + }, + { + content: "Verify category shows child links as oe_direct_line", + trigger: + '.o_form_view .o_field_widget[name="content_parsed"] a.oe_direct_line', + timeout: 20000, + run: function () { + var links = document.querySelectorAll( + '.o_field_widget[name="content_parsed"] a.oe_direct_line' + ); + if (links.length < 1) { + throw new Error("Category should have child page links"); + } + // Verify links have correct attributes + var link = links[0]; + if (!link.dataset.oeModel || !link.dataset.oeId) { + throw new Error("Category index link missing data-oe-model/id"); + } + if (link.getAttribute("target") === "_blank") { + throw new Error("Category index link should not open in new tab"); + } + }, + }, + ] +); diff --git a/document_page_reference/tests/__init__.py b/document_page_reference/tests/__init__.py index ca802a6bb24..05ff47b536c 100644 --- a/document_page_reference/tests/__init__.py +++ b/document_page_reference/tests/__init__.py @@ -1 +1,2 @@ from . import test_document_reference +from . import test_document_reference_tour diff --git a/document_page_reference/tests/test_document_reference.py b/document_page_reference/tests/test_document_reference.py index 4f16744490d..7e372c8e9c1 100644 --- a/document_page_reference/tests/test_document_reference.py +++ b/document_page_reference/tests/test_document_reference.py @@ -75,4 +75,4 @@ def test_get_formview_action(self): def test_compute_content_parsed(self): self.page1.content = "

    " self.page1._compute_content_parsed() - self.assertEqual(str(self.page1.content_parsed), "

    ") + self.assertEqual(str(self.page1.content_parsed), "

    ") diff --git a/document_page_reference/tests/test_document_reference_tour.py b/document_page_reference/tests/test_document_reference_tour.py new file mode 100644 index 00000000000..2176767e11b --- /dev/null +++ b/document_page_reference/tests/test_document_reference_tour.py @@ -0,0 +1,58 @@ +# Copyright 2025 TRIVAX INNOVA SL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import HttpCase, tagged + + +@tagged("post_install", "-at_install") +class TestDocumentPageReferenceTour(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Ensure admin password is known for the tour + cls.env["res.users"].browse(2).write({"password": "admin"}) + page_obj = cls.env["document.page"] + # Create a category + cls.category = page_obj.create( + { + "name": "Test Ref Category", + "type": "category", + } + ) + # Create two pages with cross-references + cls.page1 = page_obj.create( + { + "name": "Test Ref Page 1", + "parent_id": cls.category.id, + "content": "

    Go to ${r2} for details.

    ", + "reference": "r1", + } + ) + cls.page2 = page_obj.create( + { + "name": "Test Ref Page 2", + "parent_id": cls.category.id, + "content": "

    Back to ${r1} for overview.

    ", + "reference": "r2", + } + ) + + def test_reference_widget_renders_links(self): + """Test that the document_page_reference widget renders ${ref} + as clickable links and navigates without opening new tabs.""" + self.start_tour( + "/web#action=document_page.action_page", + "document_page_reference_widget_tour", + login="admin", + timeout=60, + ) + + def test_category_index_links(self): + """Test that category pages show child links as oe_direct_line + without duplicating content.""" + self.start_tour( + "/web#action=document_page.action_page", + "document_page_reference_category_tour", + login="admin", + timeout=60, + ) diff --git a/document_page_reference/views/document_page.xml b/document_page_reference/views/document_page.xml index a9459fcceb2..15997029f21 100644 --- a/document_page_reference/views/document_page.xml +++ b/document_page_reference/views/document_page.xml @@ -15,16 +15,19 @@ /> - - oe_edit_only - + + + {'invisible': [('type', '=', 'category')], + 'required': [('type', '!=', 'category')]} + + @@ -32,16 +35,16 @@ document.page - - oe_edit_only - + + 1 +