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 Oct 22, 2024
1 parent 4bf23f3 commit 7cc2d9d
Show file tree
Hide file tree
Showing 19 changed files with 637 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;
}
}
156 changes: 156 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,156 @@
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
) {
archInfo.parentFieldColumn = 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);
}
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: Array[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>
34 changes: 34 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,34 @@
import {ListController} from "@web/views/list/list_controller";
import {useChildSubEnv} from "@odoo/owl";

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 view persists the
// breadcrumb info in the global state if the environment variable
useChildSubEnv({
breadcrumbState: this.props.globalState?.breadcrumbState || [],
});
}

get modelParams() {
const modelParams = super.modelParams;
modelParams.parentFieldColumn = this.archInfo.parentFieldColumn;
return modelParams;
}

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}`
];
}
}
}
14 changes: 14 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,14 @@
<?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>
</xpath>
</t>

</templates>
31 changes: 31 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,31 @@
import {RelationalModel} from "@web/model/relational_model/relational_model";

export class HierarchyListModel extends RelationalModel {
setup(params) {
super.setup(...arguments);
this.parentFieldColumn = params.parentFieldColumn;
}

/**
* @param {*} currentConfig
* @param {*} params
* @returns {Config}
*/
_getNextConfig(currentConfig, params) {
const nextConfig = super._getNextConfig(...arguments);
if ("hierarchyListParentRecord" in params) {
nextConfig.domain = params.hierarchyListDomain;
nextConfig.domain = [
[
this.parentFieldColumn.name,
"=",
params.hierarchyListParentRecord.resId,
],
];
this.env.searchModel.globalContext[
`default_${this.parentFieldColumn.name}`
] = params.hierarchyListParentRecord.resId;
}
return nextConfig;
}
}
Loading

0 comments on commit 7cc2d9d

Please sign in to comment.