diff --git a/recursive_tree_view/__manifest__.py b/recursive_tree_view/__manifest__.py
index dd28c5b..ce10ad0 100644
--- a/recursive_tree_view/__manifest__.py
+++ b/recursive_tree_view/__manifest__.py
@@ -30,11 +30,7 @@
"data": [],
"assets": {
"web.assets_backend": [
- "recursive_tree_view/static/src/list_arch_parser.js",
- "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",
+ "recursive_tree_view/static/src/**/*",
]
},
"installable": True,
diff --git a/recursive_tree_view/static/src/list_arch_parser.js b/recursive_tree_view/static/src/list_arch_parser.js
index eef4ad4..20abd27 100644
--- a/recursive_tree_view/static/src/list_arch_parser.js
+++ b/recursive_tree_view/static/src/list_arch_parser.js
@@ -10,14 +10,7 @@ patch(ListArchParser.prototype, {
const recursiveAttr = xmlDoc.getAttribute("recursive")
if ( recursiveAttr ) {
result.recursive = recursiveAttr === "1" || recursiveAttr === "true" || recursiveAttr === "True";
- result.childField = xmlDoc.getAttribute("child-field")
-
- if (!(result.childField in result)) {
-
- }
}
-
-
return result;
}
})
\ No newline at end of file
diff --git a/recursive_tree_view/static/src/list_controller.js b/recursive_tree_view/static/src/list_controller.js
index 21d5a4e..50ed66f 100644
--- a/recursive_tree_view/static/src/list_controller.js
+++ b/recursive_tree_view/static/src/list_controller.js
@@ -2,80 +2,36 @@
import {ListController} from '@web/views/list/list_controller';
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";
patch(ListController.prototype, {
async setup() {
- useBus(this.env.bus, "expandRow", this.onExpandRow)
- useBus(this.env.bus, "collapseRow", this.onCollapseRow)
- super.setup();
if (this.props.archInfo.recursive) {
- // Fetch the parent field from the model
- const parentField = await this.orm.call(
- 'parent.field.service',
- 'get_parent_field',
- [this.props.resModel],
- );
+ this.recursive = true;
+ useBus(this.env.bus, "expand-collapse-parent", this.onExpandCollapseParent);
+ } else {
+ this.recursive = false;
+ }
+ super.setup();
+ },
- // Validate the parentField and adjust archInfo accordingly
- if (parentField) {
- // If parentField is valid, store it and continue with recursive setup
- this.parentField = parentField;
- this.props.archInfo.parentField = parentField;
- } else {
- // If no valid parentField, disable recursion and childField functionality
- this.props.archInfo.recursive = false;
- 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 onExpandCollapseParent(ev) {
+ const parentId = ev.detail;
+ const record = this.model.findRecordInHierarchy(parentId);
+ if (record.expanded) {
+ record.expanded = false;
+ } else {
+ await this.model._loadChildren(record.children);
+ record.expanded = true;
}
},
- async fetchChildren(parentId) {
- if (this.childrenByParent[parentId]) {
- return this.childrenByParent[parentId];
+ get modelParams() {
+ const params = super.modelParams;
+ if (this.recursive) {
+ params["config"]["recursive"] = true;
}
-
- 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;
+ return params;
},
- 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);
- },
});
diff --git a/recursive_tree_view/static/src/list_renderer.js b/recursive_tree_view/static/src/list_renderer.js
index dddd651..873d41a 100644
--- a/recursive_tree_view/static/src/list_renderer.js
+++ b/recursive_tree_view/static/src/list_renderer.js
@@ -2,58 +2,18 @@
import { ListRenderer } from '@web/views/list/list_renderer';
import { patch } from '@web/core/utils/patch';
+import { useState } from '@odoo/owl';
patch(ListRenderer.prototype, {
setup() {
super.setup();
this.recursive = this.props.archInfo.recursive;
+ useState(this.props.list.records);
},
async _onExpandClick(ev) {
const $button = $(ev.currentTarget);
- const isExpanded = $button.hasClass('expanded');
- $button.toggleClass('expanded', !isExpanded).text(isExpanded ? '+' : '-');
-
- 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 = $('
')
- .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 = $('').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();
+ const parent = $button.data('expand');
+ this.env.bus.trigger("expand-collapse-parent", parent);
},
});
diff --git a/recursive_tree_view/static/src/recursive_tree_templates.xml b/recursive_tree_view/static/src/recursive_tree_templates.xml
index 0b06253..757e150 100644
--- a/recursive_tree_view/static/src/recursive_tree_templates.xml
+++ b/recursive_tree_view/static/src/recursive_tree_templates.xml
@@ -1,21 +1,39 @@
-
+
- |
+
+
+ |
-
+
+
+
+
+
+
+
+
-
-
-
|
diff --git a/recursive_tree_view/static/src/relational_model.js b/recursive_tree_view/static/src/relational_model.js
index d7773ad..4a66f70 100644
--- a/recursive_tree_view/static/src/relational_model.js
+++ b/recursive_tree_view/static/src/relational_model.js
@@ -2,40 +2,67 @@
import {RelationalModel} from '@web/model/relational_model/relational_model';
import {patch} from '@web/core/utils/patch';
-import {
- getFieldsSpec,
- makeActiveField,
- getBasicEvalContext
-} from "@web/model/relational_model/utils";
+import {getFieldsSpec, getBasicEvalContext} from "@web/model/relational_model/utils";
-patch(RelationalModel.prototype, 'recursive-list-extension', {
- /**
- * Override to add child record tracking.
- * Fetches records and marks records with `hasChildren` if they have child records.
- */
- async _loadRecords(config, evalContext = config.context) {
- const {resModel, resIds, activeFields, fields, context} = config;
- const parentField = await this.orm.call(
- 'parent.field.service',
- 'get_parent_field',
- [resModel],
- );
- activeFields[parentField] = makeActiveField()
- const records = await super._loadRecords(config, evalContext);
- // Get the parent ID field if there is one
- if (records && parentField) {
- const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);
- // Fetch records with the additional field
- const parentIds = resIds
- const children = await this.orm.webSearchRead(resModel, [[parentField, "in", parentIds]], {
- context: {bin_size: true, ...context},
+patch(RelationalModel.prototype, {
+ // TODO: Modify the domain to include only records with parentField = False in the root search
+
+ setup(params, services) {
+ super.setup(...arguments);
+ this.hooks.onRootLoaded = () => {
+ const root = this.root;
+ const config = this.root.config;
+ if (config.recursive && root.records) {
+ this._loadChildren(root.records, config).then(() => {
+ return root;
+ });
+ }
+ }
+ },
+ async _loadData(config) {
+ if (config.recursive) {
+ const parentField = await this._get_parent_field(config.resModel);
+ const domain = [parentField, '=', false];
+ if (!(domain in config.domain)) {
+ 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,
});
+ if (children && children.length) {
// Track children by grouping child records under each parent
const childrenByParent = {};
- for (const child of children) {
- const parentId = child[parentField];
+ for (const child of children.records) {
+ const parentId = child[parentField].id;
if (parentId) {
if (!childrenByParent[parentId]) {
childrenByParent[parentId] = [];
@@ -44,37 +71,69 @@ patch(RelationalModel.prototype, 'recursive-list-extension', {
}
}
- records.forEach(record => {
- record.children = new this.constructor.DynamicRecordList(this, config, childrenByParent[record.resId]);
- });
+ for (const parent of records) {
+ 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],
+ );
},
- /**
- * Fetch and load child records dynamically for a given parent record ID.
- * Adds children to the model’s records to ensure they’re tracked in the model’s state.
- * @param {number} parentId - The ID of the parent record.
- * @returns {Promise} - List of child records as model records.
- */
- async fetchChildren(parentId) {
- const config = self.config
- const {resModel, resIds, activeFields, fields, context} = config;
- const evalContext = getBasicEvalContext(config);
- const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);
- const parentRecord = this.root.records.find((r) => r.resId == parentId)
- if (!parentRecord) {
- throw Error("Attempt to find parent record failed.");
+ findRecordInHierarchy(resId) {
+ const records = this.root.records;
+ function findRecord(records) {
+ for (let record of records) {
+ if (record.resId === resId) {
+ return record;
+ }
+ if (record.children && record.children.length >0) {
+ const found = findRecord(record.children);
+ if (found) {
+ return found;
+ }
+ }
+ }
}
- // Fetch child records where `parent_id` matches the given `parentId`
- 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;
- },
-
+ return findRecord(records) || null;
+ }
});
diff --git a/recursive_tree_view/static/src/tree_recursive_styles.css b/recursive_tree_view/static/src/tree_recursive_styles.css
index dc1b411..c4425c4 100644
--- a/recursive_tree_view/static/src/tree_recursive_styles.css
+++ b/recursive_tree_view/static/src/tree_recursive_styles.css
@@ -2,14 +2,27 @@
width: 30px;
}
-tr[data-depth="1"] .o_recursive_expand_column {
- padding-left: 20px;
+.o_expand_button {
+ border-color: transparent;
+ background-color: transparent;
}
-tr[data-depth="2"] .o_recursive_expand_column {
- padding-left: 40px;
+button.o_expand_button[data-depth="1"] {
+ margin-left: 15px;
}
-tr[data-depth="3"] .o_recursive_expand_column {
- padding-left: 60px;
+button.o_expand_button[data-depth="2"] {
+ 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;
}
diff --git a/recursive_tree_view/validation.py b/recursive_tree_view/validation.py
index 48d16c2..ca8de27 100644
--- a/recursive_tree_view/validation.py
+++ b/recursive_tree_view/validation.py
@@ -34,11 +34,6 @@ def schema_tree(arch, **kwargs):
_tag="{%s}attribute" % RNG_NS,
name="recursive",
)
- child_field_attr = etree.SubElement(
- optional_attr,
- _tag="{%s}attribute" % RNG_NS,
- name="child-field",
- )
# Create RelaxNG validator from the modified schema
_tree_validator = etree.RelaxNG(rng_doc)
|