mirror of https://github.com/OCA/web.git
Merge 052a040d00
into 6a9a4ad63b
commit
09466dec2a
|
@ -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