working recursive tree view
This commit is contained in:
parent
ae086d645c
commit
6065636123
8 changed files with 186 additions and 196 deletions
|
|
@ -30,11 +30,7 @@
|
||||||
"data": [],
|
"data": [],
|
||||||
"assets": {
|
"assets": {
|
||||||
"web.assets_backend": [
|
"web.assets_backend": [
|
||||||
"recursive_tree_view/static/src/list_arch_parser.js",
|
"recursive_tree_view/static/src/**/*",
|
||||||
"recursive_tree_view/static/src/list_controller.js",
|
|
||||||
"recursive_tree_view/static/src/list_renderer.js",
|
|
||||||
"recursive_tree_view/static/src/tree_recursive_styles.css",
|
|
||||||
"recursive_tree_view/static/src/recursive_tree_templates.xml",
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"installable": True,
|
"installable": True,
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,7 @@ patch(ListArchParser.prototype, {
|
||||||
const recursiveAttr = xmlDoc.getAttribute("recursive")
|
const recursiveAttr = xmlDoc.getAttribute("recursive")
|
||||||
if ( recursiveAttr ) {
|
if ( recursiveAttr ) {
|
||||||
result.recursive = recursiveAttr === "1" || recursiveAttr === "true" || recursiveAttr === "True";
|
result.recursive = recursiveAttr === "1" || recursiveAttr === "true" || recursiveAttr === "True";
|
||||||
result.childField = xmlDoc.getAttribute("child-field")
|
|
||||||
|
|
||||||
if (!(result.childField in result)) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -2,80 +2,36 @@
|
||||||
|
|
||||||
import {ListController} from '@web/views/list/list_controller';
|
import {ListController} from '@web/views/list/list_controller';
|
||||||
import {patch} from '@web/core/utils/patch';
|
import {patch} from '@web/core/utils/patch';
|
||||||
import {useService} from '@web/core/utils/hooks';
|
|
||||||
import {onWillRender} from '@odoo/owl';
|
|
||||||
import {useBus} from "@web/core/utils/hooks";
|
import {useBus} from "@web/core/utils/hooks";
|
||||||
|
|
||||||
patch(ListController.prototype, {
|
patch(ListController.prototype, {
|
||||||
async setup() {
|
async setup() {
|
||||||
useBus(this.env.bus, "expandRow", this.onExpandRow)
|
|
||||||
useBus(this.env.bus, "collapseRow", this.onCollapseRow)
|
|
||||||
super.setup();
|
|
||||||
if (this.props.archInfo.recursive) {
|
if (this.props.archInfo.recursive) {
|
||||||
// Fetch the parent field from the model
|
this.recursive = true;
|
||||||
const parentField = await this.orm.call(
|
useBus(this.env.bus, "expand-collapse-parent", this.onExpandCollapseParent);
|
||||||
'parent.field.service',
|
} else {
|
||||||
'get_parent_field',
|
this.recursive = false;
|
||||||
[this.props.resModel],
|
}
|
||||||
);
|
super.setup();
|
||||||
|
},
|
||||||
|
|
||||||
// Validate the parentField and adjust archInfo accordingly
|
async onExpandCollapseParent(ev) {
|
||||||
if (parentField) {
|
const parentId = ev.detail;
|
||||||
// If parentField is valid, store it and continue with recursive setup
|
const record = this.model.findRecordInHierarchy(parentId);
|
||||||
this.parentField = parentField;
|
if (record.expanded) {
|
||||||
this.props.archInfo.parentField = parentField;
|
record.expanded = false;
|
||||||
} else {
|
} else {
|
||||||
// If no valid parentField, disable recursion and childField functionality
|
await this.model._loadChildren(record.children);
|
||||||
this.props.archInfo.recursive = false;
|
record.expanded = true;
|
||||||
delete this.props.archInfo.childField;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed with recursive setup if recursive is enabled
|
|
||||||
if (this.props.archInfo.recursive) {
|
|
||||||
this.childrenByParent = {}; // Cache for loaded children
|
|
||||||
|
|
||||||
// Bind event listeners for expand and collapse
|
|
||||||
this.model.hooks.onRootLoaded = async () => {
|
|
||||||
const rootRecords = this.model.root.records;
|
|
||||||
const rootIds = rootRecords.map(record => record.resId)
|
|
||||||
const childRecords = await this.orm.searchRead(
|
|
||||||
this.props.resModel,
|
|
||||||
[[this.parentField, 'in', rootIds]],
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
const rootIdsWithChildren = new Set(childRecords.map(child => child[this.parentField]));
|
|
||||||
rootRecords.forEach(record => {
|
|
||||||
record.data.hasChildren = rootIdsWithChildren.has(record.data.resId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchChildren(parentId) {
|
get modelParams() {
|
||||||
if (this.childrenByParent[parentId]) {
|
const params = super.modelParams;
|
||||||
return this.childrenByParent[parentId];
|
if (this.recursive) {
|
||||||
|
params["config"]["recursive"] = true;
|
||||||
}
|
}
|
||||||
|
return params;
|
||||||
const children = await this.rpc({
|
|
||||||
model: this.props.resModel,
|
|
||||||
method: 'search_read',
|
|
||||||
args: [[this.parentField, '=', parentId]],
|
|
||||||
kwargs: {fields: ['id', 'name', this.parentField]},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.childrenByParent[parentId] = children;
|
|
||||||
return children;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async onExpandRow(event) {
|
|
||||||
const {parentId} = event.data;
|
|
||||||
const children = await this.fetchChildren(parentId);
|
|
||||||
this.renderer.renderChildrenRows(children, parentId);
|
|
||||||
},
|
|
||||||
|
|
||||||
onCollapseRow(event) {
|
|
||||||
const {parentId} = event.data;
|
|
||||||
this.renderer.removeChildrenRows(parentId);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,58 +2,18 @@
|
||||||
|
|
||||||
import { ListRenderer } from '@web/views/list/list_renderer';
|
import { ListRenderer } from '@web/views/list/list_renderer';
|
||||||
import { patch } from '@web/core/utils/patch';
|
import { patch } from '@web/core/utils/patch';
|
||||||
|
import { useState } from '@odoo/owl';
|
||||||
|
|
||||||
patch(ListRenderer.prototype, {
|
patch(ListRenderer.prototype, {
|
||||||
setup() {
|
setup() {
|
||||||
super.setup();
|
super.setup();
|
||||||
this.recursive = this.props.archInfo.recursive;
|
this.recursive = this.props.archInfo.recursive;
|
||||||
|
useState(this.props.list.records);
|
||||||
},
|
},
|
||||||
|
|
||||||
async _onExpandClick(ev) {
|
async _onExpandClick(ev) {
|
||||||
const $button = $(ev.currentTarget);
|
const $button = $(ev.currentTarget);
|
||||||
const isExpanded = $button.hasClass('expanded');
|
const parent = $button.data('expand');
|
||||||
$button.toggleClass('expanded', !isExpanded).text(isExpanded ? '+' : '-');
|
this.env.bus.trigger("expand-collapse-parent", parent);
|
||||||
|
|
||||||
const parentId = $button.data('expand');
|
|
||||||
if (!isExpanded) {
|
|
||||||
// Emit an event to request the controller to expand the row
|
|
||||||
this.env.bus.trigger('expandRow', { parentId });
|
|
||||||
} else {
|
|
||||||
// Emit an event to request the controller to collapse the row
|
|
||||||
this.env.bus.trigger('collapseRow', { parentId });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderChildrenRows(children, parentId) {
|
|
||||||
const $parentRow = this.$(`tr[data-id="${parentId}"]`);
|
|
||||||
const parentDepth = $parentRow.data('depth') || 0;
|
|
||||||
|
|
||||||
children.forEach(child => {
|
|
||||||
const $row = $('<tr>')
|
|
||||||
.attr('data-id', child.id)
|
|
||||||
.attr('data-parent-id', parentId)
|
|
||||||
.attr('data-depth', parentDepth + 1)
|
|
||||||
.attr('data-has-children', child.hasChildren)
|
|
||||||
.addClass('o_recursive_child_row')
|
|
||||||
.css('padding-left', `${(parentDepth + 1) * 20}px`);
|
|
||||||
|
|
||||||
for (const [fieldName, fieldValue] of Object.entries(child)) {
|
|
||||||
const $cell = $('<td>').text(fieldValue);
|
|
||||||
$row.append($cell);
|
|
||||||
}
|
|
||||||
|
|
||||||
$parentRow.after($row);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
removeChildrenRows(parentId) {
|
|
||||||
const childRows = this.$(`tr[data-parent-id="${parentId}"]`);
|
|
||||||
|
|
||||||
childRows.each((index, childRow) => {
|
|
||||||
const childId = $(childRow).data('id');
|
|
||||||
this.removeChildrenRows(childId);
|
|
||||||
});
|
|
||||||
|
|
||||||
childRows.remove();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,39 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<!-- Extend List Renderer Header to Add Extra Column for Expand/Collapse Button Alignment -->
|
<!-- Extend List Renderer Header to Add Extra Column for Expand/Collapse Button Alignment -->
|
||||||
<t t-name="recursive_tree_view.Renderer" t-inherit="web.ListRenderer" t-inherit-mode="extension">
|
<t t-name="recursive_tree_view.Renderer" t-inherit="web.ListRenderer"
|
||||||
|
t-inherit-mode="extension">
|
||||||
<xpath expr="//th[hasclass('o_list_record_selector')]" position="before">
|
<xpath expr="//th[hasclass('o_list_record_selector')]" position="before">
|
||||||
<th t-if="recursive" class="o_recursive_expand_column"></th>
|
<th t-if="recursive"
|
||||||
|
class="o_recursive_expand_column cursor-default o_list_button">
|
||||||
|
<div style="min-width: 100px;"/>
|
||||||
|
</th>
|
||||||
</xpath>
|
</xpath>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
<!-- Extend List Renderer Row to Add Expand/Collapse Button -->
|
<!-- Extend List Renderer Row to Add Expand/Collapse Button -->
|
||||||
<t t-name="recursive_tree_view.RecordRow" t-inherit="web.ListRenderer.RecordRow" t-inherit-mode="extension">
|
<t t-name="web.ListRenderer.RecordRow" t-inherit="web.ListRenderer.RecordRow"
|
||||||
|
t-inherit-mode="extension">
|
||||||
|
<xpath expr="//tr[hasclass('o_data_row')]" position="after">
|
||||||
|
<t t-if="record.children && record.expanded">
|
||||||
|
<t t-foreach="record.children" t-as="record" t-key="record.id">
|
||||||
|
<t t-call="{{ constructor.recordRowTemplate }}"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
<xpath expr="//td[1]" position="before">
|
<xpath expr="//td[1]" position="before">
|
||||||
<td t-if="this.recursive" class="o_recursive_expand_column">
|
<td t-if="this.recursive"
|
||||||
<!-- Use @click to bind the button click to _onExpandClick -->
|
class="o_recursive_expand_column"
|
||||||
<button t-if="record.data.hasChildren"
|
>
|
||||||
t-att-data-expand="props.record.id.raw_value"
|
<button t-if="record.children"
|
||||||
|
t-att-data-expand="record.resId"
|
||||||
t-on-click.prevent="_onExpandClick"
|
t-on-click.prevent="_onExpandClick"
|
||||||
class="o_expand_button">+</button>
|
t-att-data-depth="record.depth"
|
||||||
|
class="o_expand_button">
|
||||||
|
<span role="img"
|
||||||
|
t-att-class="'fa ' + (record.expanded ? 'fa-angle-down' : 'fa-angle-right')"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</xpath>
|
</xpath>
|
||||||
</t>
|
</t>
|
||||||
|
|
|
||||||
|
|
@ -2,40 +2,67 @@
|
||||||
|
|
||||||
import {RelationalModel} from '@web/model/relational_model/relational_model';
|
import {RelationalModel} from '@web/model/relational_model/relational_model';
|
||||||
import {patch} from '@web/core/utils/patch';
|
import {patch} from '@web/core/utils/patch';
|
||||||
import {
|
import {getFieldsSpec, getBasicEvalContext} from "@web/model/relational_model/utils";
|
||||||
getFieldsSpec,
|
|
||||||
makeActiveField,
|
|
||||||
getBasicEvalContext
|
|
||||||
} from "@web/model/relational_model/utils";
|
|
||||||
|
|
||||||
patch(RelationalModel.prototype, 'recursive-list-extension', {
|
patch(RelationalModel.prototype, {
|
||||||
/**
|
// TODO: Modify the domain to include only records with parentField = False in the root search
|
||||||
* Override to add child record tracking.
|
|
||||||
* Fetches records and marks records with `hasChildren` if they have child records.
|
setup(params, services) {
|
||||||
*/
|
super.setup(...arguments);
|
||||||
async _loadRecords(config, evalContext = config.context) {
|
this.hooks.onRootLoaded = () => {
|
||||||
const {resModel, resIds, activeFields, fields, context} = config;
|
const root = this.root;
|
||||||
const parentField = await this.orm.call(
|
const config = this.root.config;
|
||||||
'parent.field.service',
|
if (config.recursive && root.records) {
|
||||||
'get_parent_field',
|
this._loadChildren(root.records, config).then(() => {
|
||||||
[resModel],
|
return root;
|
||||||
);
|
});
|
||||||
activeFields[parentField] = makeActiveField()
|
}
|
||||||
const records = await super._loadRecords(config, evalContext);
|
}
|
||||||
// Get the parent ID field if there is one
|
},
|
||||||
if (records && parentField) {
|
async _loadData(config) {
|
||||||
const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);
|
if (config.recursive) {
|
||||||
// Fetch records with the additional field
|
const parentField = await this._get_parent_field(config.resModel);
|
||||||
const parentIds = resIds
|
const domain = [parentField, '=', false];
|
||||||
const children = await this.orm.webSearchRead(resModel, [[parentField, "in", parentIds]], {
|
if (!(domain in config.domain)) {
|
||||||
context: {bin_size: true, ...context},
|
config.domain = config.domain.concat([domain]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super._loadData(config);
|
||||||
|
},
|
||||||
|
async _loadChildren(records, config = undefined) {
|
||||||
|
if (!records) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!config) {
|
||||||
|
config = this.config;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(records)) {
|
||||||
|
records = [records];
|
||||||
|
}
|
||||||
|
const {resModel, activeFields, fields, context} = config;
|
||||||
|
if (!resModel) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const parentField = await this._get_parent_field(resModel);
|
||||||
|
if (!parentField) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const evalContext = getBasicEvalContext(config);
|
||||||
|
const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);
|
||||||
|
// Fetch records with the additional field
|
||||||
|
const parentIds = records.map(record => record.resId);
|
||||||
|
|
||||||
|
const children = await this.orm.webSearchRead(
|
||||||
|
resModel, [[parentField, "in", parentIds]], {
|
||||||
|
context: {...context},
|
||||||
specification: fieldSpec,
|
specification: fieldSpec,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (children && children.length) {
|
||||||
// Track children by grouping child records under each parent
|
// Track children by grouping child records under each parent
|
||||||
const childrenByParent = {};
|
const childrenByParent = {};
|
||||||
for (const child of children) {
|
for (const child of children.records) {
|
||||||
const parentId = child[parentField];
|
const parentId = child[parentField].id;
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
if (!childrenByParent[parentId]) {
|
if (!childrenByParent[parentId]) {
|
||||||
childrenByParent[parentId] = [];
|
childrenByParent[parentId] = [];
|
||||||
|
|
@ -44,37 +71,69 @@ patch(RelationalModel.prototype, 'recursive-list-extension', {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
records.forEach(record => {
|
for (const parent of records) {
|
||||||
record.children = new this.constructor.DynamicRecordList(this, config, childrenByParent[record.resId]);
|
if (parent.depth == undefined) {
|
||||||
});
|
parent.depth = 0;
|
||||||
|
}
|
||||||
|
if (parent.expanded == undefined) {
|
||||||
|
parent.expanded = false;
|
||||||
|
}
|
||||||
|
if (!parent.childrenFetched) {
|
||||||
|
if (childrenByParent[parent.resId]) {
|
||||||
|
const childRecords = childrenByParent[parent.resId];
|
||||||
|
parent.children = []
|
||||||
|
for (const child of childRecords) {
|
||||||
|
const childRecord = new this.constructor.Record(
|
||||||
|
this,
|
||||||
|
{
|
||||||
|
context: context,
|
||||||
|
activeFields: activeFields,
|
||||||
|
resModel: resModel,
|
||||||
|
fields: fields,
|
||||||
|
resId: child.id,
|
||||||
|
resIds: [child.id],
|
||||||
|
isMonoRecord: true,
|
||||||
|
currentCompanyId: parent.currentCompanyId,
|
||||||
|
mode: parent.mode,
|
||||||
|
},
|
||||||
|
child,
|
||||||
|
{manuallyAdded: false},
|
||||||
|
);
|
||||||
|
parent.children.push(childRecord);
|
||||||
|
childRecord.parent = parent;
|
||||||
|
childRecord.depth = parent.depth + 1;
|
||||||
|
childRecord.expanded = false;
|
||||||
|
childRecord.childrenFetched = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent.childrenFetched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return records;
|
},
|
||||||
|
async _get_parent_field(model) {
|
||||||
|
return await this.orm.call(
|
||||||
|
'parent.field.service',
|
||||||
|
'get_parent_field',
|
||||||
|
[model],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
findRecordInHierarchy(resId) {
|
||||||
* Fetch and load child records dynamically for a given parent record ID.
|
const records = this.root.records;
|
||||||
* Adds children to the model’s records to ensure they’re tracked in the model’s state.
|
function findRecord(records) {
|
||||||
* @param {number} parentId - The ID of the parent record.
|
for (let record of records) {
|
||||||
* @returns {Promise<Array>} - List of child records as model records.
|
if (record.resId === resId) {
|
||||||
*/
|
return record;
|
||||||
async fetchChildren(parentId) {
|
}
|
||||||
const config = self.config
|
if (record.children && record.children.length >0) {
|
||||||
const {resModel, resIds, activeFields, fields, context} = config;
|
const found = findRecord(record.children);
|
||||||
const evalContext = getBasicEvalContext(config);
|
if (found) {
|
||||||
const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);
|
return found;
|
||||||
const parentRecord = this.root.records.find((r) => r.resId == parentId)
|
}
|
||||||
if (!parentRecord) {
|
}
|
||||||
throw Error("Attempt to find parent record failed.");
|
}
|
||||||
}
|
}
|
||||||
// Fetch child records where `parent_id` matches the given `parentId`
|
return findRecord(records) || null;
|
||||||
const childrenData = await this.orm.webSearchRead(resModel, [[this.parentField, '=', parentId]], {
|
}
|
||||||
context: context,
|
|
||||||
specification: fieldSpec,
|
|
||||||
});
|
|
||||||
if (childrenData) {
|
|
||||||
parentRecord.children = new this.constructor.DynamicRecordList(this, this.config, childrenData);
|
|
||||||
}
|
|
||||||
return parentRecord.children;
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,27 @@
|
||||||
width: 30px;
|
width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr[data-depth="1"] .o_recursive_expand_column {
|
.o_expand_button {
|
||||||
padding-left: 20px;
|
border-color: transparent;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr[data-depth="2"] .o_recursive_expand_column {
|
button.o_expand_button[data-depth="1"] {
|
||||||
padding-left: 40px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr[data-depth="3"] .o_recursive_expand_column {
|
button.o_expand_button[data-depth="2"] {
|
||||||
padding-left: 60px;
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.o_expand_button[data-depth="3"] {
|
||||||
|
margin-left: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.o_expand_button[data-depth="4"] {
|
||||||
|
margin-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.o_expand_button[data-depth="5"] {
|
||||||
|
margin-left: 75px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,6 @@ def schema_tree(arch, **kwargs):
|
||||||
_tag="{%s}attribute" % RNG_NS,
|
_tag="{%s}attribute" % RNG_NS,
|
||||||
name="recursive",
|
name="recursive",
|
||||||
)
|
)
|
||||||
child_field_attr = etree.SubElement(
|
|
||||||
optional_attr,
|
|
||||||
_tag="{%s}attribute" % RNG_NS,
|
|
||||||
name="child-field",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create RelaxNG validator from the modified schema
|
# Create RelaxNG validator from the modified schema
|
||||||
_tree_validator = etree.RelaxNG(rng_doc)
|
_tree_validator = etree.RelaxNG(rng_doc)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue