Display Role Hierarchy using Lightning Tree

Display role hierarchy using lightning tree component and apex controller to prepare JSON to support tree format.

Recently while navigating through lightning component library, I landed on lightning:tree tag. As soon as I saw this, I found two use cases where we can use lightning tree component. Those are as follows:

  • Displaying role hierarchy
  • Showing list of accounts in hierarchy using parent child relationship

So decided to give a try. I built the lightning component to show role hierarchy in tree structure format, similar to the standard one. You can extend it further as per your business need. Logic will be same for second use case, showing account hierarchy, with minor changes in field and object APIs.

I have shared the Github repo at the bottom of this article.

How it looks like?

Role hierarchy using lightning tree
Role hierarchy using lightning tree

This involves one lightning component, one aura application and of course an apex class.

Lets deep dive into components:

Roles_Tree.cmp :

  • Declared the variable “items” which will be used by lightning tree tag to render list of roles in tree structure.
  • Referring to controller RoleTreeController to get the list of role .
<aura:component controller="RoleTreeController">
    <aura:handler name="init" value="{!this}" action="{!c.init}" />
    <aura:attribute name="items" type="Object" access="PRIVATE"/>
    <aura:attribute name="isExpanded" type="boolean" default="true"/>
    <aura:attribute name="selectedID" type="Id" />
    <div class="slds-grid slds-grid_vertical slds-gutters">
        <div class="slds-col slds-align-middle slds-p-vertical_large">
            <div class="slds-text-heading_large slds-text-align_center">Role Heirarchy - Using Lightning:tree</div>
        </div>
        <div class="slds-col slds-align-middle slds-p-vertical_large">
            <div class="slds-box slds-box_small">
            	<lightning:tree items="{! v.items }" header="Roles" onselect="{!c.handleSelect}"/>
            </div>
            <aura:if isTrue="{!not(empty(v.selectedID))}">
            	<div class="slds-text-body_regular slds-p-vertical_small">Selected Role ID : {!v.selectedID}</div>
            </aura:if>
        </div>
        <div class="slds-col slds-align-middle">
            <div class="slds-text-body_regular">Project by <b><a href="https://sfdcdevelopers.com">Vinay Vernekar</a></b></div>
        </div>
    </div>
</aura:component>

Roles_TreeController.js :

  • init method is being called on component load, which again calling fetchRoles method from helper.
  • handleSelect method is setting the id of role on selecting the role name from tree.
({
    init : function(component, event, helper) {
         helper.fetchRoles(component);
    },
    handleSelect: function (component, event, helper) {
        component.set("v.selectedID", event.getParam('name'));
    }
})

Roles_TreeHelper.js

  • fetchRoles method calling getTreeJSON method from apex class RoleTreeController to fetch the list of roles on initial load of component.
({
	fetchRoles : function(component) {
             var action = component.get('c.getTreeJSON'); 
             action.setCallback(this, function(ret){
                var state = ret.getState(); // get the response state
                if(state == 'SUCCESS') {
                   let items = JSON.parse(ret.getReturnValue());
                   component.set("v.items", items);
                }
                else{
                   alert('Error occured while querying roles');
                }
           });
           $A.enqueueAction(action);
	}
})

Roles_Tree_App.app :

  • Roles_Tree component is placed in the app
  • extends=”force:slds” is important to load lightning component UI correctly
<aura:application extends="force:slds">
	<c:Roles_Tree></c:Roles_Tree>
</aura:application>

RoleTreeController.cls

  • getTreeJson is the aura enabled method being called from lightning component helper.
  • init is the static method being called from static code block, executing before the execution of getTreeJson method. It’s responsible to prepare map of parent and its children roles.
  • createNode is the method responsible to add parent and its child recursively calling itself till the last role present in hierarchy.
  • convertNodeToJSON method converts the map to the json required by lightning:tree. You can checkout official documentation. JSON parameter “expanded” at line 94 can be set to “true” or “false” if you want role hierarchy expanded at the start or not.
/*****************************************************
 * Name : RoleTreeController
 * Developer: Vinay Vernekar
 * Website: https://sfdcdevelopers.com
 * Email: [email protected]
 * Purpose: Utility class to fetch roles and subordinates
 * Date: 16th April 2020
*******************************************************/
public class RoleTreeController {
    
    // map to hold roles with Id as the key
    private static Map <Id, UserRole> rolesMap;
    
    // map to hold child roles with parentRoleId as the key
    private static Map <Id, List<UserRole>> parentChildRoleMap;
    
    //parent role id
    private static Id ParentRoleId;
    
    // Global JSON generator
    private static JSONGenerator gen;

    /* // initialize helper data */ 
    static {
        init();
    }
    
    /* init starts <to initialise helper data> */
    private static void init() {
        
        // Get role to users mapping in a map with key as role id
        rolesMap = new Map<Id, UserRole>([select Id, Name, parentRoleId from UserRole PortalType = 'None' order by Name]);
        
        // populate parent role - child roles map
        parentChildRoleMap = new Map <Id, List<UserRole>>();        
        for (UserRole r : rolesMap.values()) {
            if(ParentRoleId == null)
                ParentRoleId = R.Id;
            List<UserRole> tempList;
            if (!parentChildRoleMap.containsKey(r.parentRoleId)){
                tempList = new List<UserRole>();
                tempList.Add(r);
                parentChildRoleMap.put(r.parentRoleId, tempList);
            }
            else {
                tempList = (List<UserRole>)parentChildRoleMap.get(r.parentRoleId);
                tempList.add(r);
                parentChildRoleMap.put(r.parentRoleId, tempList);
            }
        }
    } 
    /* init ends */

    /* createNode starts */
    private static RoleNodeWrapper createNode(Id objId) {
        RoleNodeWrapper n = new RoleNodeWrapper();
        n.myRoleId = objId;
        n.myRoleName = rolesMap.get(objId).Name;
        
        if (parentChildRoleMap.containsKey(objId)){
            n.hasChildren = true;
            n.isLeafNode = false;
            List<RoleNodeWrapper> lst = new List<RoleNodeWrapper>();
            for (UserRole r : parentChildRoleMap.get(objId)) {
                lst.add(createNode(r.Id));
            }           
            n.myChildNodes = lst;
        }
        else {
            n.isLeafNode = true;
            n.hasChildren = false;
        }
        return n;
    }
    
    /* Invoke function from lightning component */
    @AuraEnabled
    public static String getTreeJSON() {
        gen = JSON.createGenerator(true);
        RoleNodeWrapper node = createNode(ParentRoleId);
        gen.writeStartArray();
        convertNodeToJSON(node);
        gen.writeEndArray();
        return gen.getAsString();
    }
    
    /* Convert tree structure to JSON */
    private static void convertNodeToJSON(RoleNodeWrapper objRNW){
        gen.writeStartObject();
        gen.writeStringField('label', objRNW.myRoleName);
        gen.writeStringField('name', objRNW.myRoleId);
        gen.writeBooleanField('expanded', true);
        if (objRNW.hasChildren){
            gen.writeFieldName('items');
            gen.writeStartArray();
            if (objRNW.hasChildren)
            {
                for (RoleNodeWrapper r : objRNW.myChildNodes){
                    convertNodeToJSON(r);
                }
            }
            gen.writeEndArray();
        }
        gen.writeEndObject();
    }
    
    public class RoleNodeWrapper {
        public String myRoleName {get; set;}
        public Id myRoleId {get; set;}
        public Boolean hasChildren {get; set;}
        public Boolean isLeafNode {get; set;}
        public List<RoleNodeWrapper> myChildNodes {get; set;}
        public RoleNodeWrapper(){
            hasChildren = false;
        }
    }
}

Further Enhancements:

  • Modify the method getTreeJson to only consider roles below the logged in user.
  • Current apex logic includes the roles for internal users. You can change the SOQL query (at line 33 of apex class) to include only customer portal roles as follows:
select Id, Name, parentRoleId from UserRole where PortalType = 'CustomerPortal' order by Name

PortalType can have any of the following value:

  • None: Salesforce application role.
  • CustomerPortal: Customer portal role.
  • Partner: partner portal role. The field IsPartner used in release 8.0 will map to this value.

Github Repo:

Clone and deploy the code from repo: https://github.com/SFDCDevs/Role-Heirarchy-Aura-Component.git

Thanks To:

Apex Logic Reference : http://forceguru.blogspot.com/2011/12/displaying-role-hierarchy-on.html