diff --git a/addons/web/static/src/views/fields/x2many/x2many_field.js b/addons/web/static/src/views/fields/x2many/x2many_field.js index c38a75d28f6..b2f2f274675 100644 --- a/addons/web/static/src/views/fields/x2many/x2many_field.js +++ b/addons/web/static/src/views/fields/x2many/x2many_field.js @@ -17,6 +17,7 @@ import { KanbanRenderer } from "@web/views/kanban/kanban_renderer"; import { ListRenderer } from "@web/views/list/list_renderer"; import { computeViewClassName } from "@web/views/utils"; import { ViewButton } from "@web/views/view_button/view_button"; +import { useService } from "@web/core/utils/hooks"; import { Component } from "@odoo/owl"; @@ -123,6 +124,7 @@ export class X2ManyField extends Component { ]; return selectCreate(p); }; + this.action = useService("action"); } get activeField() { @@ -221,9 +223,25 @@ export class X2ManyField extends Component { !this.props.readonly && ("editable" in params ? params.editable : editable); this.onAdd(params); }; + const openFormView = props.editable ? archInfo.openFormView : false; + props.onOpenFormView = openFormView ? this.switchToForm.bind(this) : undefined; return props; } + switchToForm(record) { + this.action.doAction( + { + type: "ir.actions.act_window", + views: [[false, "form"]], + res_id: record.resId, + res_model: this.list.resModel, + }, + { + props: { resIds: this.list.resIds }, + } + ); + } + async onAdd({ context, editable } = {}) { const domain = typeof this.props.domain === "function" ? this.props.domain() : this.props.domain; 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 f4b3671c0f2..d63d481c4b8 100644 --- a/addons/web/static/src/views/list/list_arch_parser.js +++ b/addons/web/static/src/views/list/list_arch_parser.js @@ -188,6 +188,10 @@ export class ListArchParser extends XMLParser { ? archParseBoolean(node.getAttribute("multi_edit") || "") : false; + treeAttr.openFormView = treeAttr.editable + ? archParseBoolean(xmlDoc.getAttribute("open_form_view") || "") + : false; + const limitAttr = node.getAttribute("limit"); treeAttr.limit = limitAttr && parseInt(limitAttr, 10); diff --git a/addons/web/static/src/views/list/list_controller.js b/addons/web/static/src/views/list/list_controller.js index c0ac3a6a2c9..33c7b509854 100644 --- a/addons/web/static/src/views/list/list_controller.js +++ b/addons/web/static/src/views/list/list_controller.js @@ -47,6 +47,8 @@ export class ListController extends Component { this.rootRef = useRef("root"); this.archInfo = this.props.archInfo; + const openFormView = this.props.editable ? this.archInfo.openFormView : false; + this.onOpenFormView = openFormView ? this.openRecord.bind(this) : undefined; this.activeActions = this.archInfo.activeActions; this.editable = this.activeActions.edit && this.props.editable ? this.archInfo.editable : false; diff --git a/addons/web/static/src/views/list/list_controller.xml b/addons/web/static/src/views/list/list_controller.xml index 68c8e9cc600..25f55725a45 100644 --- a/addons/web/static/src/views/list/list_controller.xml +++ b/addons/web/static/src/views/list/list_controller.xml @@ -75,6 +75,7 @@ archInfo="archInfo" allowSelectors="props.allowSelectors" editable="editable" + onOpenFormView="onOpenFormView" openRecord.bind="openRecord" noContentHelp="props.info.noContentHelp" onAdd.bind="createRecord" diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js index 6b9c3bea335..ae3b99e0bd5 100644 --- a/addons/web/static/src/views/list/list_renderer.js +++ b/addons/web/static/src/views/list/list_renderer.js @@ -269,7 +269,11 @@ export class ListRenderer extends Component { return { ...getPropertyFieldInfo(propertyField), id: `${column.id}_${propertyField.name}`, - column_invisible: combineModifiers(propertyField.column_invisible, column.column_invisible, 'OR'), + column_invisible: combineModifiers( + propertyField.column_invisible, + column.column_invisible, + "OR" + ), classNames: column.classNames, optional: "hide", type: "field", @@ -437,6 +441,9 @@ export class ListRenderer extends Component { if (this.activeActions.onDelete || this.displayOptionalFields) { nbCols++; } + if (this.props.onOpenFormView) { + nbCols++; + } return nbCols; } @@ -571,8 +578,9 @@ export class ListRenderer extends Component { get optionalFieldGroups() { const propertyGroups = {}; const optionalFields = []; - const optionalColumns = this.allColumns.filter((col) => - col.optional && !this.evalColumnInvisible(col.column_invisible)); + const optionalColumns = this.allColumns.filter( + (col) => col.optional && !this.evalColumnInvisible(col.column_invisible) + ); for (const col of optionalColumns) { const optionalField = { label: col.label, @@ -597,8 +605,9 @@ export class ListRenderer extends Component { } get hasOptionalFields() { - return this.allColumns.some((col) => - col.optional && !this.evalColumnInvisible(col.column_invisible)); + return this.allColumns.some( + (col) => col.optional && !this.evalColumnInvisible(col.column_invisible) + ); } get displayOptionalFields() { @@ -996,6 +1005,9 @@ export class ListRenderer extends Component { if (this.hasSelectors) { colspan++; } + if (this.props.onOpenFormView) { + colspan++; + } return colspan; } @@ -2122,6 +2134,7 @@ ListRenderer.props = [ "cycleOnTab?", "allowSelectors?", "editable?", + "onOpenFormView?", "noContentHelp?", "nestedKeyOptionalFieldsData?", "onOptionalFieldsChanged?", diff --git a/addons/web/static/src/views/list/list_renderer.scss b/addons/web/static/src/views/list/list_renderer.scss index 72aac79d4c4..d670d69c9bb 100644 --- a/addons/web/static/src/views/list/list_renderer.scss +++ b/addons/web/static/src/views/list/list_renderer.scss @@ -73,7 +73,7 @@ } tbody > tr > td:not(.o_list_record_selector) { - &:not(.o_handle_cell):not(.o_list_button):not(.o_list_record_remove) { + &:not(.o_handle_cell):not(.o_list_button):not(.o_list_record_remove):not(.o_list_record_open_form_view) { @include o-text-overflow(table-cell); &.o_list_text { white-space: pre-wrap; @@ -148,7 +148,7 @@ width: 40px !important; // Force to 40px regardless by the font-size } - .o_list_record_remove, .o_handle_cell { + .o_list_record_remove, .o_handle_cell, .o_list_record_open_form_view { width: 1px; // to prevent the column to expand } @@ -159,7 +159,7 @@ } } - .o_list_record_remove button { + .o_list_record_remove button, .o_list_record_open_form_view button { padding: 0px; background: none; border-style: none; @@ -167,6 +167,10 @@ cursor: pointer; } + .o_list_record_remove button, button { + padding-top: 5px; + } + .o_keyboard_navigation { th:focus-within, td:focus-within { --table-accent-bg: none; @@ -259,8 +263,14 @@ cursor: col-resize; } + .o_list_open_form_view { + width: 64px; + min-width: 64px + } + .o_list_actions_header { width: 32px; + min-width: 32px } .o_data_row.o_list_no_open { diff --git a/addons/web/static/src/views/list/list_renderer.xml b/addons/web/static/src/views/list/list_renderer.xml index eeff5f8ac73..b21255dad2c 100644 --- a/addons/web/static/src/views/list/list_renderer.xml +++ b/addons/web/static/src/views/list/list_renderer.xml @@ -41,7 +41,8 @@ - + + + @@ -266,6 +268,20 @@ + + + + + + diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index 7b6de804160..449975e8be9 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -10295,6 +10295,52 @@ QUnit.module("Views", (hooks) => { ); }); + QUnit.test("Can switch to form view on inline tree", async function (assert) { + const id = 2; + const actionService = { + start() { + return { + doAction(action, options) { + assert.step("doAction"); + assert.deepEqual(action, { + res_id: id, + res_model: "partner", + type: "ir.actions.act_window", + views: [[false, "form"]], + }); + assert.deepEqual(options.props, { + resIds: [id], + }); + }, + }; + }, + }; + registry.category("services").add("action", actionService, { force: true }); + + serverData.models.partner.records[0].p = [id]; + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
+ + + + + +
`, + resId: 1, + }); + assert.containsOnce( + target, + "td.o_list_record_open_form_view", + "button to open form view should be present" + ); + await click(target.querySelector("td.o_list_record_open_form_view")); + assert.verifySteps(["doAction"]); + }); + QUnit.test("can toggle column in x2many in sub form view", async function (assert) { serverData.models.partner.records[2].p = [1, 2]; serverData.models.partner.fields.foo.sortable = true; diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index e301b6432b3..babbdde5b76 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -630,6 +630,54 @@ QUnit.module("Views", (hooks) => { assert.verifySteps(["switch to form - resId: 1 activeIds: 1,2,3,4"]); }); + QUnit.test("non-editable list with open_form_view", async function (assert) { + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + }); + assert.containsNone( + target, + "td.o_list_record_open_form_view", + "button to open form view should not be present on non-editable list" + ); + }); + + QUnit.test("editable list with open_form_view not set", async function (assert) { + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + }); + assert.containsNone( + target, + "td.o_list_record_open_form_view", + "button to open form view should not be present" + ); + }); + + QUnit.test("editable list with open_form_view", async function (assert) { + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + selectRecord: (resId, options) => { + assert.step(`switch to form - resId: ${resId} activeIds: ${options.activeIds}`); + }, + }); + assert.containsN( + target, + "td.o_list_record_open_form_view", + 4, + "button to open form view should be present on each rows" + ); + await click(target.querySelector("td.o_list_record_open_form_view")); + assert.verifySteps(["switch to form - resId: 1 activeIds: 1,2,3,4"]); + }); + QUnit.test( "export feature in list for users not in base.group_allow_export", async function (assert) { @@ -4774,7 +4822,7 @@ QUnit.module("Views", (hooks) => { 25, "Currency field should have a fixed width of 25px (see arch)" ); - assert.strictEqual(target.querySelector(".o_list_actions_header").style.width, "32px"); + assert.strictEqual(target.querySelector(".o_list_actions_header").offsetWidth, 32); } ); diff --git a/odoo/addons/base/rng/tree_view.rng b/odoo/addons/base/rng/tree_view.rng index db3c77ea109..6c2f30dded0 100644 --- a/odoo/addons/base/rng/tree_view.rng +++ b/odoo/addons/base/rng/tree_view.rng @@ -49,6 +49,7 @@ +