[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:
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user