diff --git a/addons/web/static/src/views/list/list_arch_parser.js b/addons/web/static/src/views/list/list_arch_parser.js index f945dcdb423..38f26d9a331 100644 --- a/addons/web/static/src/views/list/list_arch_parser.js +++ b/addons/web/static/src/views/list/list_arch_parser.js @@ -10,6 +10,8 @@ import { } from "@web/views/utils"; import { Field } from "@web/views/fields/field"; import { XMLParser } from "@web/core/utils/xml"; +import { Widget } from "@web/views/widgets/widget"; +import { encodeObjectForTemplate } from "@web/views/view_compiler"; export class GroupListArchParser extends XMLParser { parse(arch, models, modelName, jsClass) { @@ -104,6 +106,27 @@ export class ListArchParser extends XMLParser { }); } return false; + } else if (node.tagName === "widget") { + const widgetInfo = Widget.parseWidgetNode(node); + addFieldDependencies( + activeFields, + models[modelName], + widgetInfo.WidgetComponent.fieldDependencies + ); + + const widgetProps = { + ...widgetInfo, + // FIXME: this is dumb, we encode it into a weird object so that the widget + // can decode it later... + node: encodeObjectForTemplate({ attrs: widgetInfo.rawAttrs }).slice(1, -1), + className: node.getAttribute("class"), + }; + columns.push({ + ...widgetInfo, + props: widgetProps, + id: `column_${nextId++}`, + type: "widget", + }); } else if (node.tagName === "groupby" && node.getAttribute("name")) { const fieldName = node.getAttribute("name"); const xmlSerializer = new XMLSerializer(); diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js index abd7ba100b2..aa50bd27eab 100644 --- a/addons/web/static/src/views/list/list_renderer.js +++ b/addons/web/static/src/views/list/list_renderer.js @@ -17,6 +17,7 @@ import { getTooltipInfo } from "@web/views/fields/field_tooltip"; import { getClassNameFromDecoration } from "@web/views/utils"; import { ViewButton } from "@web/views/view_button/view_button"; import { useBounceButton } from "@web/views/view_hook"; +import { Widget } from "@web/views/widgets/widget"; const { Component, @@ -1747,7 +1748,7 @@ ListRenderer.rowsTemplate = "web.ListRenderer.Rows"; ListRenderer.recordRowTemplate = "web.ListRenderer.RecordRow"; ListRenderer.groupRowTemplate = "web.ListRenderer.GroupRow"; -ListRenderer.components = { DropdownItem, Field, ViewButton, CheckBox, Dropdown, Pager }; +ListRenderer.components = { DropdownItem, Field, ViewButton, CheckBox, Dropdown, Pager, Widget }; ListRenderer.props = [ "activeActions?", "list", diff --git a/addons/web/static/src/views/list/list_renderer.xml b/addons/web/static/src/views/list/list_renderer.xml index 69f3ac37425..f4689a43034 100644 --- a/addons/web/static/src/views/list/list_renderer.xml +++ b/addons/web/static/src/views/list/list_renderer.xml @@ -247,6 +247,9 @@ + + + diff --git a/addons/web/static/src/views/widgets/widget.js b/addons/web/static/src/views/widgets/widget.js index 5f853fce6d6..1efd5e23af7 100644 --- a/addons/web/static/src/views/widgets/widget.js +++ b/addons/web/static/src/views/widgets/widget.js @@ -60,5 +60,15 @@ Widget.template = xml/*xml*/ ` Widget.parseWidgetNode = function (node) { const name = node.getAttribute("name"); const WidgetComponent = findWidgetComponent(name); - return { WidgetComponent }; + const attrs = Object.fromEntries( + [...node.attributes].map(({ name, value }) => { + return [name, name === "modifiers" ? JSON.parse(value || "{}") : value]; + }) + ); + return { + options: evaluateExpr(node.getAttribute("options") || "{}"), + name, + rawAttrs: attrs, + WidgetComponent, + }; }; diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 25d20794f33..30c6a0a5c73 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -251,7 +251,7 @@ export class MockServer { doc = arch; } const editableView = editable && this._editableNode(doc, modelName); - const inTreeView = doc.tagName === "tree"; + const inTreeView = ["tree", "list"].includes(doc.tagName); const inFormView = doc.tagName === "form"; // mock _postprocess_access_rights const isBaseModel = !context.base_model_name || modelName === context.base_model_name; diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 88b274f571f..e41f9c8f306 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -15514,4 +15514,27 @@ QUnit.module("Views", (hooks) => { assert.containsOnce(target, "div[name=text] textarea:focus"); assert.containsOnce(target, ".o_selected_row div[name=text]"); }); + + QUnit.test("view widgets are rendered in list view", async function (assert) { + class TestWidget extends Component {} + TestWidget.template = xml`
`; + registry.category("view_widgets").add("test_widget", TestWidget); + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: ` + + + + + `, + }); + assert.containsN(target, ".test_widget", 4, "there should be one widget per record"); + assert.deepEqual( + [...target.querySelectorAll(".test_widget")].map((w) => w.textContent), + ["true", "true", "true", "false"], + "the widget has access to the record's data" + ); + }); });