mirror of https://github.com/OCA/web.git
18.0 [ADD] web_hierarchy_list
parent
4bf23f33a1
commit
052a040d00
|
@ -0,0 +1,24 @@
|
|||
# Copyright 2024 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "Web Hierarchy List",
|
||||
"summary": """
|
||||
This modules adds the hierarchy list view, which consist of a list view
|
||||
and a breadcrumb.
|
||||
""",
|
||||
"version": "18.0.1.0.0",
|
||||
"license": "AGPL-3",
|
||||
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/web",
|
||||
"depends": [
|
||||
"web",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"web_hierarchy_list/static/src/**/*",
|
||||
],
|
||||
},
|
||||
"data": [],
|
||||
"demo": [],
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
|
@ -0,0 +1,10 @@
|
|||
import {ListArchParser} from "@web/views/list/list_arch_parser";
|
||||
import {treatHierarchyListArch} from "./hierarchy_list_arch_utils.esm";
|
||||
|
||||
export class HierarchyListArchParser extends ListArchParser {
|
||||
parse(xmlDoc, models, modelName) {
|
||||
const archInfo = super.parse(...arguments);
|
||||
treatHierarchyListArch(archInfo, modelName, models[modelName].fields);
|
||||
return archInfo;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
const isParentFieldOptionsName = "isParentField";
|
||||
const isChildrenFieldOptionsName = "isChildrenField";
|
||||
const isNameFieldOptionsName = "isNameField";
|
||||
|
||||
function _handleIsParentFieldOption(archInfo, modelName, fields, column) {
|
||||
if (archInfo.parentFieldColumn) {
|
||||
throw new Error(
|
||||
`The ${isParentFieldOptionsName} field option is already present in the view definition.`
|
||||
);
|
||||
}
|
||||
if (fields[column.name].type !== "many2one") {
|
||||
throw new Error(
|
||||
`Invalid field for ${isParentFieldOptionsName} field option, it should be a Many2One field.`
|
||||
);
|
||||
} else if (fields[column.name].relation !== modelName) {
|
||||
throw new Error(
|
||||
`Invalid field for ${isParentFieldOptionsName} field option, the co-model should be same model than the current one (expected: ${modelName}).`
|
||||
);
|
||||
}
|
||||
if ("drillDownCondition" in column.options) {
|
||||
archInfo.drillDownCondition = column.options.drillDownCondition;
|
||||
}
|
||||
if ("drillDownIcon" in column.options) {
|
||||
archInfo.drillDownIcon = column.options.drillDownIcon;
|
||||
}
|
||||
archInfo.parentFieldColumn = column;
|
||||
}
|
||||
|
||||
function _handleIsChildrenFieldOption(archInfo, modelName, fields, column) {
|
||||
if (archInfo.childrenFieldColumn) {
|
||||
throw new Error(
|
||||
`The ${isChildrenFieldOptionsName} field option is already present in the view definition.`
|
||||
);
|
||||
}
|
||||
if (fields[column.name].type !== "one2many") {
|
||||
throw new Error(
|
||||
`Invalid field for ${isChildrenFieldOptionsName} field option, it should be a One2Many field.`
|
||||
);
|
||||
} else if (fields[column.name].relation !== modelName) {
|
||||
throw new Error(
|
||||
`Invalid field for ${isChildrenFieldOptionsName} field option, the co-model should be same model than the current one (expected: ${modelName}).`
|
||||
);
|
||||
}
|
||||
archInfo.childrenFieldColumn = column;
|
||||
}
|
||||
|
||||
function _handleIsNameFieldOption(archInfo, modelName, fields, column) {
|
||||
if (archInfo.nameFieldColumn) {
|
||||
throw new Error(
|
||||
`The ${isNameFieldOptionsName} field option is already present in the view definition.`
|
||||
);
|
||||
}
|
||||
archInfo.nameFieldColumn = column;
|
||||
}
|
||||
|
||||
function _handleParentFieldColumnFallback(archInfo, modelName, fields, columnDict) {
|
||||
const parentIdFieldName = "parent_id";
|
||||
if (!archInfo.parentFieldColumn) {
|
||||
if (
|
||||
parentIdFieldName in fields &&
|
||||
fields[parentIdFieldName].type === "many2one" &&
|
||||
fields[parentIdFieldName].relation === modelName
|
||||
) {
|
||||
_handleIsParentFieldOption(
|
||||
archInfo,
|
||||
modelName,
|
||||
fields,
|
||||
columnDict[parentIdFieldName]
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Neither ${parentIdFieldName} field is present in the view fields, nor is ${isParentFieldOptionsName} field option defined on a field.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _handleChildrenFieldColumnFallback(archInfo, modelName, fields, columnDict) {
|
||||
const childIdsFieldName = "child_ids";
|
||||
if (!archInfo.childrenFieldColumn) {
|
||||
if (
|
||||
childIdsFieldName in fields &&
|
||||
fields[childIdsFieldName].type === "one2many" &&
|
||||
fields[childIdsFieldName].relation === modelName
|
||||
) {
|
||||
archInfo.childrenFieldColumn = columnDict[childIdsFieldName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _handleNameFieldColumnFallback(archInfo, modelName, fields, columnDict) {
|
||||
const displayNameFieldName = "display_name";
|
||||
if (!archInfo.nameFieldColumn) {
|
||||
if (displayNameFieldName in fields) {
|
||||
archInfo.nameFieldColumn = columnDict[displayNameFieldName];
|
||||
} else {
|
||||
throw new Error(
|
||||
`Neither ${displayNameFieldName} field is present in the view fields, nor is ${isNameFieldOptionsName} field option defined on a field.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _handleDrillDownConditionFallback(archInfo) {
|
||||
if (!archInfo.drillDownCondition && archInfo.childrenFieldColumn) {
|
||||
archInfo.drillDownCondition = `${archInfo.childrenFieldColumn.name}.length > 0`;
|
||||
}
|
||||
}
|
||||
|
||||
function _handleParentFieldColumnVisibility(archInfo) {
|
||||
if (archInfo.parentFieldColumn) {
|
||||
// The column tagged as parent field is made invisible, except id explicitly set otherwise.
|
||||
if (
|
||||
!["invisible", "column_invisible"].some(
|
||||
(value) =>
|
||||
![null, undefined].includes(archInfo.parentFieldColumn[value])
|
||||
)
|
||||
) {
|
||||
archInfo.parentFieldColumn.column_invisible = "1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _handleChildrenFieldColumnVisibility(archInfo) {
|
||||
if (archInfo.childrenFieldColumn) {
|
||||
// The column tagged as children field is made invisible, except id explicitly set otherwise.
|
||||
if (
|
||||
!["invisible", "column_invisible"].some(
|
||||
(value) =>
|
||||
![null, undefined].includes(archInfo.childrenFieldColumn[value])
|
||||
)
|
||||
) {
|
||||
archInfo.childrenFieldColumn.column_invisible = "1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function treatHierarchyListArch(archInfo, modelName, fields) {
|
||||
const columnDict = {};
|
||||
|
||||
for (const column of archInfo.columns) {
|
||||
columnDict[column.name] = column;
|
||||
if (column.options) {
|
||||
if (column.options[isParentFieldOptionsName]) {
|
||||
_handleIsParentFieldOption(archInfo, modelName, fields, column);
|
||||
}
|
||||
if (column.options[isChildrenFieldOptionsName]) {
|
||||
_handleIsChildrenFieldOption(archInfo, modelName, fields, column);
|
||||
}
|
||||
if (column.options[isNameFieldOptionsName]) {
|
||||
_handleIsNameFieldOption(archInfo, modelName, fields, column);
|
||||
}
|
||||
}
|
||||
}
|
||||
_handleParentFieldColumnFallback(archInfo, modelName, fields, columnDict);
|
||||
_handleChildrenFieldColumnFallback(archInfo, modelName, fields, columnDict);
|
||||
_handleNameFieldColumnFallback(archInfo, modelName, fields, columnDict);
|
||||
_handleDrillDownConditionFallback(archInfo);
|
||||
_handleParentFieldColumnVisibility(archInfo);
|
||||
_handleChildrenFieldColumnVisibility(archInfo);
|
||||
// Inline Edition is not supported (yet?)
|
||||
archInfo.activeActions.edit = false;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import {Component} from "@odoo/owl";
|
||||
import {HierarchyListBreadcrumbItem} from "./hierarchy_list_breadcrumb_item.esm";
|
||||
|
||||
export class HierarchyListBreadcrumb extends Component {
|
||||
static components = {
|
||||
HierarchyListBreadcrumbItem,
|
||||
};
|
||||
static props = {
|
||||
parentRecords: {type: Array, element: Object},
|
||||
getDisplayName: Function,
|
||||
navigate: Function,
|
||||
reset: Function,
|
||||
};
|
||||
static template = "web_hierarchy_list.Breadcrumb";
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
|
||||
<t t-name="web_hierarchy_list.Breadcrumb">
|
||||
<div class="d-flex flex-row o_hierarchy_list_breadcrumb container-fluid py-2">
|
||||
<nav aria-label="breadcrumb">
|
||||
<i
|
||||
class="fa fa-times pe-2 cursor-pointer"
|
||||
t-on-click="props.reset"
|
||||
t-if="props.parentRecords.length > 0"
|
||||
/>
|
||||
<ol class="breadcrumb bg-transparent d-inline-flex">
|
||||
<t
|
||||
t-foreach="props.parentRecords"
|
||||
t-as="parentRecord"
|
||||
t-key="parentRecord.resId"
|
||||
>
|
||||
<HierarchyListBreadcrumbItem
|
||||
record="parentRecord"
|
||||
getDisplayName="props.getDisplayName"
|
||||
navigate="props.navigate"
|
||||
/>
|
||||
</t>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
|
@ -0,0 +1,14 @@
|
|||
import {Component} from "@odoo/owl";
|
||||
|
||||
export class HierarchyListBreadcrumbItem extends Component {
|
||||
static props = {
|
||||
record: Object,
|
||||
getDisplayName: Function,
|
||||
navigate: Function,
|
||||
};
|
||||
static template = "web_hierarchy_list.BreadcrumbItem";
|
||||
|
||||
onGlobalClick() {
|
||||
this.props.navigate(this.props.record);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
|
||||
<t t-name="web_hierarchy_list.BreadcrumbItem">
|
||||
<li class="breadcrumb-item">
|
||||
<strong><span
|
||||
class="cursor-pointer text-primary"
|
||||
t-out="props.getDisplayName(props.record)"
|
||||
t-on-click.synthetic="onGlobalClick"
|
||||
/></strong>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
</templates>
|
|
@ -0,0 +1,45 @@
|
|||
import {onWillUnmount, useChildSubEnv} from "@odoo/owl";
|
||||
import {ListController} from "@web/views/list/list_controller";
|
||||
|
||||
export class HierarchyListController extends ListController {
|
||||
static template = "web_hierarchy_list.HierarchyListView";
|
||||
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.parentRecord = false;
|
||||
// Initializing breadcrumbState to an empty array is important as the HierarchyListRender
|
||||
// persists the breadcrumb state in the global state only if the environment variable
|
||||
// is set. This restriction is put in place in order not to persist the state when
|
||||
// the HierarchyListRender is mounted on a x2Many Field.
|
||||
useChildSubEnv({
|
||||
breadcrumbState: this.props.globalState?.breadcrumbState || [],
|
||||
});
|
||||
onWillUnmount(this.onWillUnmount);
|
||||
}
|
||||
|
||||
async onWillUnmount() {
|
||||
delete this.actionService.currentController.action.context[
|
||||
`default_${this.archInfo.parentFieldColumn.name}`
|
||||
];
|
||||
}
|
||||
|
||||
async onParentRecordUpdate(parentRecord) {
|
||||
if (parentRecord) {
|
||||
this.actionService.currentController.action.context[
|
||||
`default_${this.archInfo.parentFieldColumn.name}`
|
||||
] = parentRecord.resId;
|
||||
} else {
|
||||
delete this.actionService.currentController.action.context[
|
||||
`default_${this.archInfo.parentFieldColumn.name}`
|
||||
];
|
||||
}
|
||||
const hierarchyListParentIdDomain = [
|
||||
[this.props.archInfo.parentFieldColumn.name, "=", parentRecord.resId],
|
||||
];
|
||||
await this.model.load({hierarchyListParentIdDomain});
|
||||
}
|
||||
|
||||
async onBreadcrumbReset() {
|
||||
await this.env.searchModel._notify();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
|
||||
<t
|
||||
t-name="web_hierarchy_list.HierarchyListView"
|
||||
t-inherit="web.ListView"
|
||||
t-inherit-mode="primary"
|
||||
>
|
||||
<xpath expr="//t[@t-component='props.Renderer']" position="attributes">
|
||||
<attribute name="onParentRecordUpdate.bind">onParentRecordUpdate</attribute>
|
||||
<attribute name="onBreadcrumbReset.bind">onBreadcrumbReset</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
|
@ -0,0 +1,18 @@
|
|||
import {RelationalModel} from "@web/model/relational_model/relational_model";
|
||||
|
||||
export class HierarchyListModel extends RelationalModel {
|
||||
/**
|
||||
* @param {*} currentConfig
|
||||
* @param {*} params
|
||||
* @returns {Config}
|
||||
*/
|
||||
_getNextConfig(currentConfig, params) {
|
||||
const nextConfig = super._getNextConfig(...arguments);
|
||||
// As we need to display records according to the drill-down, we need a way to pass
|
||||
// the info to the model, which is performed through the use of the hierarchyListParentIdDomain
|
||||
if ("hierarchyListParentIdDomain" in params) {
|
||||
nextConfig.domain = params.hierarchyListParentIdDomain;
|
||||
}
|
||||
return nextConfig;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import {onWillStart, useState} from "@odoo/owl";
|
||||
import {HierarchyListBreadcrumb} from "./hierarchy_list_breadcrumb.esm";
|
||||
import {ListRenderer} from "@web/views/list/list_renderer";
|
||||
import {evaluateBooleanExpr} from "@web/core/py_js/py";
|
||||
import {useSetupAction} from "@web/search/action_hook";
|
||||
|
||||
export class HierarchyListRenderer extends ListRenderer {
|
||||
static components = {
|
||||
...ListRenderer.components,
|
||||
HierarchyListBreadcrumb,
|
||||
};
|
||||
static props = [...ListRenderer.props, "onParentRecordUpdate", "onBreadcrumbReset"];
|
||||
static template = "web_hierarchy_list.HierarchyListRenderer";
|
||||
static rowsTemplate = "web_hierarchy_list.HierarchyListRenderer.Rows";
|
||||
static recordRowTemplate = "web_hierarchy_list.HierarchyListRenderer.RecordRow";
|
||||
setup() {
|
||||
super.setup();
|
||||
useSetupAction({
|
||||
getGlobalState: () => {
|
||||
// We only persist the breadcrumb state in the global state if it was provided
|
||||
// by the environment. Indeed, the environment variable is created by the
|
||||
// HierarchyListController, which ensures that the state is only persisted there
|
||||
// and not when the renderer is used in a x2Many field.
|
||||
if (
|
||||
!this.env.breadcrumbState ||
|
||||
this.state.breadcrumbState.length === 0
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
breadcrumbState: this._getBreadcrumbState(),
|
||||
};
|
||||
},
|
||||
});
|
||||
// As the breadcrumb state is not provided when the renderer is mounted into a x2Many
|
||||
// field, we need to have a fallback value.
|
||||
this.state = useState({
|
||||
breadcrumbState: this.env.breadcrumbState || [],
|
||||
});
|
||||
onWillStart(this.willStart);
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
if (this.state.breadcrumbState.length > 0) {
|
||||
this.navigate(
|
||||
this.state.breadcrumbState[this.state.breadcrumbState.length - 1]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_getBreadcrumbState() {
|
||||
return this.state.breadcrumbState.map((parentRecord) =>
|
||||
this._getParentRecord(parentRecord)
|
||||
);
|
||||
}
|
||||
|
||||
getDisplayName(record) {
|
||||
if (this.props.archInfo.nameFieldColumn.fieldType === "many2one") {
|
||||
return record.data[this.props.archInfo.nameFieldColumn.name][1];
|
||||
}
|
||||
return record.data[this.props.archInfo.nameFieldColumn.name];
|
||||
}
|
||||
|
||||
_getParentRecord(record) {
|
||||
const data = {};
|
||||
data[this.props.archInfo.nameFieldColumn.name] =
|
||||
record.data[this.props.archInfo.nameFieldColumn.name];
|
||||
return {resId: record.resId, data};
|
||||
}
|
||||
|
||||
_updateBreadcrumbState(record) {
|
||||
const existingRecordIndex = this.state.breadcrumbState
|
||||
.map((r) => r.resId)
|
||||
.indexOf(record.resId);
|
||||
if (existingRecordIndex >= 0)
|
||||
this.state.breadcrumbState = this.state.breadcrumbState.slice(
|
||||
0,
|
||||
existingRecordIndex + 1
|
||||
);
|
||||
else {
|
||||
this.state.breadcrumbState.push(this._getParentRecord(record));
|
||||
}
|
||||
}
|
||||
|
||||
canDrillDown(record) {
|
||||
if (!this.props.archInfo.drillDownCondition) {
|
||||
return true;
|
||||
}
|
||||
return evaluateBooleanExpr(
|
||||
this.props.archInfo.drillDownCondition,
|
||||
record.evalContextWithVirtualIds
|
||||
);
|
||||
}
|
||||
|
||||
async navigate(parent) {
|
||||
this._updateBreadcrumbState(parent);
|
||||
await this.props.onParentRecordUpdate(parent);
|
||||
}
|
||||
|
||||
async reset() {
|
||||
this.state.breadcrumbState.length = 0;
|
||||
await this.props.onBreadcrumbReset();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
$hierarchy_list_drill_down_width: 24px;
|
||||
|
||||
.o_hierarchy_list_drill_down_column_header {
|
||||
min-width: $hierarchy_list_drill_down_width;
|
||||
max-width: $hierarchy_list_drill_down_width;
|
||||
}
|
||||
|
||||
.o_hierarchy_list_drill_down_column {
|
||||
// We do not want the left padding rule of bootstrap for the first col to be applied.
|
||||
// Indeed as the column width is forced, this would result in having the icon overflowing.
|
||||
padding: 0.5rem 0.3rem !important;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
|
||||
<t
|
||||
t-name="web_hierarchy_list.HierarchyListRenderer"
|
||||
t-inherit="web.ListRenderer"
|
||||
t-inherit-mode="primary"
|
||||
>
|
||||
<xpath expr="//div[hasclass('o_list_renderer')]/*[1]" position="before">
|
||||
<HierarchyListBreadcrumb
|
||||
parentRecords="state.breadcrumbState"
|
||||
getDisplayName.bind="getDisplayName"
|
||||
navigate.bind="navigate"
|
||||
reset.bind="reset"
|
||||
/>
|
||||
</xpath>
|
||||
<xpath expr="//table[@t-ref='table']/thead//tr/th[1]" position="after">
|
||||
<th class="o_hierarchy_list_drill_down_column_header" />
|
||||
</xpath>
|
||||
|
||||
</t>
|
||||
|
||||
<t
|
||||
t-name="web_hierarchy_list.HierarchyListRenderer.Rows"
|
||||
t-inherit="web.ListRenderer.Rows"
|
||||
t-inherit-mode="primary"
|
||||
>
|
||||
<xpath
|
||||
expr="//t[@t-if='!list.isGrouped']//td[hasclass('o_field_x2many_list_row_add')]"
|
||||
position="before"
|
||||
>
|
||||
<td class="o_hierarchy_list_drill_down_column">
|
||||
</td>
|
||||
</xpath>
|
||||
<xpath
|
||||
expr="//t[@t-else]//td[hasclass('o_group_field_row_add')]"
|
||||
position="before"
|
||||
>
|
||||
<td class="o_hierarchy_list_drill_down_column">
|
||||
</td>
|
||||
</xpath>
|
||||
<xpath expr="//tr[@t-foreach='getEmptyRowIds']">
|
||||
<td class="o_hierarchy_list_drill_down_column">
|
||||
</td>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t
|
||||
t-name="web_hierarchy_list.HierarchyListRenderer.RecordRow"
|
||||
t-inherit="web.ListRenderer.RecordRow"
|
||||
t-inherit-mode="primary"
|
||||
>
|
||||
<xpath expr="//tr[@class='o_data_row']/td[1]" position="after">
|
||||
<td class="text-end o_hierarchy_list_drill_down_column">
|
||||
<i
|
||||
t-if="canDrillDown(record)"
|
||||
t-attf-class="pr-2 cursor-pointer fa {{props.archInfo.drillDownIcon || 'fa-plus-square-o'}}"
|
||||
t-on-click.stop="() => this.navigate(record)"
|
||||
/>
|
||||
</td>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
|
@ -0,0 +1,16 @@
|
|||
import {HierarchyListArchParser} from "./hierarchy_list_arch_parser.esm";
|
||||
import {HierarchyListController} from "./hierarchy_list_controller.esm";
|
||||
import {HierarchyListModel} from "./hierarchy_list_model.esm";
|
||||
import {HierarchyListRenderer} from "./hierarchy_list_renderer.esm";
|
||||
import {listView} from "@web/views/list/list_view";
|
||||
import {registry} from "@web/core/registry";
|
||||
|
||||
export const hierarchyListView = {
|
||||
...listView,
|
||||
ArchParser: HierarchyListArchParser,
|
||||
Controller: HierarchyListController,
|
||||
Model: HierarchyListModel,
|
||||
Renderer: HierarchyListRenderer,
|
||||
};
|
||||
|
||||
registry.category("views").add("hierarchy_list", hierarchyListView);
|
|
@ -0,0 +1,153 @@
|
|||
import {X2ManyField, x2ManyField} from "@web/views/fields/x2many/x2many_field";
|
||||
import {HierarchyListRenderer} from "./hierarchy_list_renderer.esm";
|
||||
import {RelationalModel} from "@web/model/relational_model/relational_model";
|
||||
import {evaluateExpr} from "@web/core/py_js/py";
|
||||
import {extractFieldsFromArchInfo} from "@web/model/relational_model/utils";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {treatHierarchyListArch} from "./hierarchy_list_arch_utils.esm";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
import {useState} from "@odoo/owl";
|
||||
|
||||
export class HierarchyListX2manyField extends X2ManyField {
|
||||
static components = {
|
||||
...X2ManyField.components,
|
||||
HierarchyListRenderer,
|
||||
};
|
||||
static template = "web_hierarchy_list.X2ManyField";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
treatHierarchyListArch(
|
||||
this.archInfo,
|
||||
this.field.relation,
|
||||
this.archInfo.fields
|
||||
);
|
||||
|
||||
// Creation and deletion of records is not supported (yet?)
|
||||
this.archInfo.activeActions.create = false;
|
||||
this.archInfo.activeActions.link = false;
|
||||
this.archInfo.activeActions.delete = false;
|
||||
|
||||
this.parentRecord = false;
|
||||
|
||||
const services = {};
|
||||
for (const key of RelationalModel.services) {
|
||||
services[key] = useService(key);
|
||||
}
|
||||
services.orm = services.orm || useService("orm");
|
||||
this.childrenModel = useState(
|
||||
new RelationalModel(this.env, this.modelParams, services)
|
||||
);
|
||||
}
|
||||
|
||||
get modelParams() {
|
||||
const {rawExpand} = this.archInfo;
|
||||
const {activeFields, fields} = extractFieldsFromArchInfo(
|
||||
this.archInfo,
|
||||
this.archInfo.fields
|
||||
);
|
||||
|
||||
const modelConfig = {
|
||||
resModel: this.field.relation,
|
||||
orderBy: this.archInfo.defaultOrderBy || [],
|
||||
groupBy: false,
|
||||
fields,
|
||||
activeFields,
|
||||
openGroupsByDefault: rawExpand
|
||||
? evaluateExpr(rawExpand, this.props.record.model.context)
|
||||
: false,
|
||||
};
|
||||
|
||||
return {
|
||||
config: modelConfig,
|
||||
state: this.props.state?.modelState,
|
||||
groupByInfo: {},
|
||||
defaultGroupBy: false,
|
||||
defaultOrderBy: this.archInfo.defaultOrder,
|
||||
limit: this.archInfo.limit || this.props.limit,
|
||||
countLimit: this.archInfo.countLimit,
|
||||
hooks: {},
|
||||
};
|
||||
}
|
||||
|
||||
get rendererProps() {
|
||||
let props = {};
|
||||
if (this.parentRecord) {
|
||||
props = {
|
||||
archInfo: this.archInfo,
|
||||
list: this.childrenModel.root,
|
||||
openRecord: this.openRecord.bind(this),
|
||||
activeActions: this.archInfo.activeActions,
|
||||
onOpenFormView: this.switchToForm.bind(this),
|
||||
};
|
||||
} else {
|
||||
props = super.rendererProps;
|
||||
}
|
||||
props.activeActions = this.archInfo.activeActions;
|
||||
return props;
|
||||
}
|
||||
|
||||
get pagerProps() {
|
||||
let list = this.list;
|
||||
if (this.parentRecord) {
|
||||
list = this.childrenModel.root;
|
||||
}
|
||||
return {
|
||||
offset: list.offset,
|
||||
limit: list.limit,
|
||||
total: list.count,
|
||||
onUpdate: async ({offset, limit}) => {
|
||||
const initialLimit = this.list.limit;
|
||||
const leaved = await list.leaveEditMode();
|
||||
if (leaved) {
|
||||
let adjustment_due_to_limit = 0;
|
||||
if (
|
||||
initialLimit === limit &&
|
||||
initialLimit === this.list.limit + 1
|
||||
) {
|
||||
// Unselecting the edited record might have abandonned it. If the page
|
||||
// size was reached before that record was created, the limit was temporarily
|
||||
// increased to keep that new record in the current page, and abandonning it
|
||||
// decreased this limit back to it's initial value, so we keep this into
|
||||
// account in the offset/limit update we're about to do.
|
||||
adjustment_due_to_limit -= 1;
|
||||
}
|
||||
await list.load({
|
||||
limit: limit + adjustment_due_to_limit,
|
||||
offset: offset + adjustment_due_to_limit,
|
||||
});
|
||||
}
|
||||
},
|
||||
withAccessKey: false,
|
||||
};
|
||||
}
|
||||
|
||||
async onParentRecordUpdate(parentRecord) {
|
||||
this.parentRecord = parentRecord;
|
||||
const context = {...this.archInfo.context};
|
||||
context[`default_${this.archInfo.parentFieldColumn.name}`] =
|
||||
this.parentRecord.resId;
|
||||
const params = {
|
||||
context,
|
||||
domain: [
|
||||
[this.archInfo.parentFieldColumn.name, "=", this.parentRecord.resId],
|
||||
],
|
||||
limit:
|
||||
(this.childrenModel.root && this.childrenModel.root.limit) ||
|
||||
this.archInfo.limit,
|
||||
};
|
||||
await this.childrenModel.load(params);
|
||||
}
|
||||
|
||||
async onBreadcrumbReset() {
|
||||
this.parentRecord = false;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
export const hierarchyListX2manyField = {
|
||||
...x2ManyField,
|
||||
component: HierarchyListX2manyField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("one2many_hierarchy_list", hierarchyListX2manyField);
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t
|
||||
t-name="web_hierarchy_list.X2ManyField"
|
||||
t-inherit="web.X2ManyField"
|
||||
t-inherit-mode="primary"
|
||||
>
|
||||
<xpath expr="//ListRenderer" position="replace">
|
||||
<HierarchyListRenderer
|
||||
t-if="props.viewMode === 'list'"
|
||||
t-props="rendererProps"
|
||||
onParentRecordUpdate.bind="onParentRecordUpdate"
|
||||
onBreadcrumbReset.bind="onBreadcrumbReset"
|
||||
/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
Loading…
Reference in New Issue