Skip to content

Commit

Permalink
18.0 [ADD] web_hierarchy_list
Browse files Browse the repository at this point in the history
  • Loading branch information
Laurent Stukkens committed Nov 7, 2024
1 parent 4bf23f3 commit 052a040
Show file tree
Hide file tree
Showing 19 changed files with 718 additions and 0 deletions.
Empty file added web_hierarchy_list/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions web_hierarchy_list/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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": [],
}
3 changes: 3 additions & 0 deletions web_hierarchy_list/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
Binary file added web_hierarchy_list/static/description/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions web_hierarchy_list/static/src/hierarchy_list_arch_parser.esm.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
163 changes: 163 additions & 0 deletions web_hierarchy_list/static/src/hierarchy_list_arch_utils.esm.js
Original file line number Diff line number Diff line change
@@ -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;
}
15 changes: 15 additions & 0 deletions web_hierarchy_list/static/src/hierarchy_list_breadcrumb.esm.js
Original file line number Diff line number Diff line change
@@ -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";
}
29 changes: 29 additions & 0 deletions web_hierarchy_list/static/src/hierarchy_list_breadcrumb.xml
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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);
}
}
14 changes: 14 additions & 0 deletions web_hierarchy_list/static/src/hierarchy_list_breadcrumb_item.xml
Original file line number Diff line number Diff line change
@@ -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>
45 changes: 45 additions & 0 deletions web_hierarchy_list/static/src/hierarchy_list_controller.esm.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
15 changes: 15 additions & 0 deletions web_hierarchy_list/static/src/hierarchy_list_controller.xml
Original file line number Diff line number Diff line change
@@ -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>
18 changes: 18 additions & 0 deletions web_hierarchy_list/static/src/hierarchy_list_model.esm.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 052a040

Please sign in to comment.