[IMP] web: allow to open a record in editable list

With this revision, it's now possible to display a button to switch from
an editable list view to a form view. It's possible by adding the
attribute open_form_view to the tree element in the list definition. It
works both with base and embedded list views.

Task-id 3063425

Part-of: odoo/odoo#116989
This commit is contained in:
Pierre Rousseau
2023-08-29 11:10:42 +00:00
parent 3e938043dc
commit 258e6a019a
10 changed files with 169 additions and 10 deletions
@@ -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;
@@ -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);
@@ -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;
@@ -75,6 +75,7 @@
archInfo="archInfo"
allowSelectors="props.allowSelectors"
editable="editable"
onOpenFormView="onOpenFormView"
openRecord.bind="openRecord"
noContentHelp="props.info.noContentHelp"
onAdd.bind="createRecord"
@@ -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?",
@@ -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 {
@@ -41,7 +41,8 @@
</th>
<th t-else="" t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev)" t-att-class="{o_list_button: column.type === 'button_group'}"/>
</t>
<th t-if="displayOptionalFields or activeActions.onDelete" t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev)" class="o_list_controller o_list_actions_header" style="width: 32px; min-width: 32px">
<th t-if="props.onOpenFormView" t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev)" class="o_list_open_form_view"/>
<th t-if="displayOptionalFields or activeActions.onDelete" t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev)" class="o_list_controller o_list_actions_header">
<Dropdown t-if="displayOptionalFields"
class="'o_optional_columns_dropdown text-center border-top-0'"
togglerClass="'btn p-0'"
@@ -87,6 +88,7 @@
</td>
<td t-else=""/>
</t>
<td t-if="props.onOpenFormView"/>
<td t-if="displayOptionalFields or activeActions.onDelete" />
</tr>
</tfoot>
@@ -266,6 +268,20 @@
</t>
</t>
<t t-if="props.onOpenFormView">
<td class="o_list_record_open_form_view text-center"
t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, group, record)"
t-on-click.stop="() => props.onOpenFormView(record)"
tabindex="-1"
>
<button class="btn btn-link text-end"
name="Open in form view"
aria-label="Open in form view"
tabindex="-1"
>View</button>
</td>
</t>
<t t-set="useUnlink" t-value="'unlink' in activeActions" />
<t t-set="hasX2ManyAction" t-value="isX2Many and (useUnlink ? activeActions.unlink : activeActions.delete)" />
<t t-if="displayOptionalFields or hasX2ManyAction">
@@ -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: `
<form>
<field name="p">
<tree editable="top" open_form_view="1">
<field name="foo"/>
</tree>
</field>
</form>`,
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;
@@ -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: '<tree open_form_view="1"><field name="foo"/></tree>',
});
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: '<tree editable="top"><field name="foo"/></tree>',
});
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: '<tree editable="top" open_form_view="1"><field name="foo"/></tree>',
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);
}
);
+1
View File
@@ -49,6 +49,7 @@
<rng:optional><rng:attribute name="sample"/></rng:optional>
<rng:optional><rng:attribute name="action"/></rng:optional>
<rng:optional><rng:attribute name="type"/></rng:optional>
<rng:optional><rng:attribute name="open_form_view"/></rng:optional>
<rng:optional>
<rng:attribute name="limit">
<rng:data type="int"/>