diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index fe4d17c210..694c26567a 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -36,7 +36,11 @@ erpnext.HierarchyChart = class { me.nodes[this.id] = this; me.make_node_element(this); - me.setup_node_click_action(this); + + if (!me.all_nodes_expanded) { + me.setup_node_click_action(this); + } + me.setup_edit_node_action(this); } }; @@ -60,8 +64,9 @@ erpnext.HierarchyChart = class { show() { frappe.breadcrumbs.add('HR'); - let me = this; + this.setup_actions(); if ($(`[data-fieldname="company"]`).length) return; + let me = this; let company = this.page.add_field({ fieldtype: 'Link', @@ -79,20 +84,9 @@ erpnext.HierarchyChart = class { // svg for connectors me.make_svg_markers(); - - if (me.$hierarchy) - me.$hierarchy.remove(); - - // setup hierarchy - me.$hierarchy = $( - ``); - - me.page.main.append(me.$hierarchy); + me.setup_hierarchy() me.render_root_nodes(); + me.all_nodes_expanded = false; } } }); @@ -101,6 +95,42 @@ erpnext.HierarchyChart = class { $(`[data-fieldname="company"]`).trigger('change'); } + setup_actions() { + let me = this; + this.page.add_inner_button(__('Expand All'), function() { + me.load_children(me.root_node, true); + me.all_nodes_expanded = true; + + me.page.remove_inner_button(__('Expand All')); + me.page.add_inner_button(__('Collapse All'), function() { + me.setup_hierarchy(); + me.render_root_nodes(); + me.all_nodes_expanded = false; + + me.page.remove_inner_button(__('Collapse All')); + me.setup_actions(); + }); + }); + } + + setup_hierarchy() { + if (this.$hierarchy) + this.$hierarchy.remove(); + + $(`#connectors`).empty(); + + // setup hierarchy + this.$hierarchy = $( + ``); + + this.page.main.append(this.$hierarchy); + this.nodes = {}; + } + make_svg_markers() { $('#arrows').remove(); @@ -126,7 +156,7 @@ erpnext.HierarchyChart = class { `); } - render_root_nodes() { + render_root_nodes(expanded_view=false) { let me = this; frappe.call({ @@ -156,7 +186,10 @@ erpnext.HierarchyChart = class { expand_node = node; }); - me.expand_node(expand_node); + if (!expanded_view) { + me.root_node = expand_node; + me.expand_node(expand_node); + } } }); } @@ -196,11 +229,20 @@ erpnext.HierarchyChart = class { $(`#${node.parent_id}`).addClass('active-path'); } - load_children(node) { - frappe.run_serially([ - () => this.get_child_nodes(node.id), - (child_nodes) => this.render_child_nodes(node, child_nodes) - ]); + load_children(node, deep=false) { + if (!deep) { + frappe.run_serially([ + () => this.get_child_nodes(node.id), + (child_nodes) => this.render_child_nodes(node, child_nodes) + ]); + } else { + frappe.run_serially([ + () => this.setup_hierarchy(), + () => this.render_root_nodes(true), + () => this.get_all_nodes(node.id, node.name), + (data_list) => this.render_children_of_all_nodes(data_list) + ]); + } } get_child_nodes(node_id) { @@ -247,6 +289,70 @@ erpnext.HierarchyChart = class { node.expanded = true; } + get_all_nodes(node_id, node_name) { + return new Promise(resolve => { + frappe.call({ + method: 'erpnext.utilities.hierarchy_chart.get_all_nodes', + args: { + method: this.method, + company: this.company, + parent: node_id, + parent_name: node_name + }, + callback: (r) => { + resolve(r.message); + } + }); + }); + } + + render_children_of_all_nodes(data_list) { + let entry = undefined; + let node = undefined; + + while(data_list.length) { + // to avoid overlapping connectors + entry = data_list.shift(); + node = this.nodes[entry.parent]; + if (node) { + this.render_child_nodes_for_expanded_view(node, entry.data); + } else { + data_list.push(entry); + } + } + } + + render_child_nodes_for_expanded_view(node, child_nodes) { + node.$children = $('') + + const last_level = this.$hierarchy.find('.level:last').index(); + const node_level = $(`#${node.id}`).parent().parent().parent().index(); + + if (last_level === node_level) { + this.$hierarchy.append(` +
  • + `); + node.$children.appendTo(this.$hierarchy.find('.level:last')); + } else { + node.$children.appendTo(this.$hierarchy.find('.level:eq(' + (node_level + 1) + ')')); + } + + node.$children.hide().empty(); + + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_node(node, data); + setTimeout(() => { + this.add_connector(node.id, data.id); + }, 250); + }); + } + + node.$children.show(); + $(`path[data-parent="${node.id}"]`).show(); + node.expanded = true; + } + add_node(node, data) { return new this.Node({ id: data.id, @@ -333,7 +439,7 @@ erpnext.HierarchyChart = class { path.setAttribute("class", "active-connector"); path.setAttribute("marker-start", "url(#arrowstart-active)"); path.setAttribute("marker-end", "url(#arrowhead-active)"); - } else if (parent.hasClass('active-path')) { + } else { path.setAttribute("class", "collapsed-connector"); path.setAttribute("marker-start", "url(#arrowstart-collapsed)"); path.setAttribute("marker-end", "url(#arrowhead-collapsed)"); diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index dd523c3443..1c2f9421fa 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -194,6 +194,7 @@ .level { margin-right: 8px; align-items: flex-start; + flex-direction: column; } #arrows { diff --git a/erpnext/utilities/hierarchy_chart.py b/erpnext/utilities/hierarchy_chart.py new file mode 100644 index 0000000000..9b0279351f --- /dev/null +++ b/erpnext/utilities/hierarchy_chart.py @@ -0,0 +1,29 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +@frappe.whitelist() +def get_all_nodes(parent, parent_name, method, company): + '''Recursively gets all data from nodes''' + method = frappe.get_attr(method) + + if not method in frappe.whitelisted: + frappe.throw(_('Not Permitted'), frappe.PermissionError) + + data = method(parent, company) + result = [dict(parent=parent, parent_name=parent_name, data=data)] + + nodes_to_expand = [{'id': d.get('id'), 'name': d.get('name')} for d in data if d.get('expandable')] + + while nodes_to_expand: + parent = nodes_to_expand.pop(0) + data = method(parent.get('id'), company) + result.append(dict(parent=parent.get('id'), parent_name=parent.get('name'), data=data)) + for d in data: + if d.get('expandable'): + nodes_to_expand.append({'id': d.get('id'), 'name': d.get('name')}) + + return result \ No newline at end of file