From 6065636123d25fed5185e67726612351d3bb5252 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Wed, 30 Oct 2024 12:11:15 -0400 Subject: [PATCH] working recursive tree view --- recursive_tree_view/__manifest__.py | 6 +- .../static/src/list_arch_parser.js | 7 - .../static/src/list_controller.js | 84 ++------- .../static/src/list_renderer.js | 48 +---- .../static/src/recursive_tree_templates.xml | 34 +++- .../static/src/relational_model.js | 173 ++++++++++++------ .../static/src/tree_recursive_styles.css | 25 ++- recursive_tree_view/validation.py | 5 - 8 files changed, 186 insertions(+), 196 deletions(-) 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 @@ - + - + +
+ - + + + + + + + + - - - + t-att-data-depth="record.depth" + class="o_expand_button"> + + 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)