Dec 31, 2011

Displaying Role Hierarchy on Visualforce Page - Tree View

Ever thought of building a tree like data structure for the users based on role hierarchy and displaying it in the form of a JavaScript tree with node selection capability on the Visualforce page?

So recently I came across a functionality where a third party javascript calendar was used on the VisualForce page and all events were fetched programmatically through Apex and plotted on the calendar. The UI looked good along with other custom developed functionality. All was fine until client asked if it was possible to select some of the logged in user's subordinates through custom VF page and plot events on the calendar for the selected users only. In other words, fetch and display only those events which were owned by users who worked below the logged in user in the role hierarchy.

It got me thinking and I did some research to check if there was an easier way to get this done, but soon realized that this required custom  and tricky Apex/VF code. There's a nice little script written by Jeff Douglas that was closest to what I actually wanted.

So I came up with this handy utility which fulfils my requirements. Getting user IDs of subordinates could also be useful in situations where, for example, you want to do a comparative analysis of performance for all users reporting to a manager.



There are mainly two parts to the solution I designed:

1. RoleUtil (Apex Class): Utility class which exposes the following API

a. public static RoleNodeWrapper getRootNodeOfUserTree (Id userOrRoleId) - function creates the tree data structure for the requested user or role ID and returns the root node to the caller
b. public static List<User> getAllSubordinates (Id userId) - function returns the list of all subordinate users for the requested user ID
c. public static String getTreeJSON (Id userOrRoleId) - function returns the JSON string for the requested user or role ID
d. public static String getSObjectTypeById(Id objectId) - general utility function to return the string representation of the object type for the requested object ID
e. public static Boolean isRole (Id objId) - internally uses the getSObjectTypeById (#d above) to check whether the requested object ID is of UserRole type
f. public class RoleNodeWrapper (inner class) - wrapper for user role, represents a node in the tree data structure mentioned above and exposes boolean properties like hasChildren, hasUsers, isLeafNode, etc

public class RoleUtil {

    /********************* Properties used by getRootNodeOfUserTree function - starts **********************/
    // map to hold roles with Id as the key
    private static Map <Id, UserRole> roleUsersMap;

    // map to hold child roles with parentRoleId as the key
    private static Map <Id, List<UserRole>> parentChildRoleMap;

    // List holds all subordinates
    private static List<User> allSubordinates {get; set;}
        
    // Global JSON generator
    private static JSONGenerator gen {get; set;}

    /********************* Properties used by getRootNodeOfUserTree function - ends **********************/
    
    
    /********************* Properties used by getSObjectTypeById function - starts ********************* */
    // map to hold global describe data
    private static Map<String,Schema.SObjectType> gd;
    
    // map to store objects and their prefixes
    private static Map<String, String> keyPrefixMap;

    // to hold set of all sObject prefixes
    private static Set<String> keyPrefixSet;
    /********************* Properties used by getSObjectTypeById function - ends **********************/
    
    /* // initialize helper data */ 
    static {
        // initialize helper data for getSObjectTypeById function
        init1();
        
        // initialize helper data for getRootNodeOfUserTree function
        init2();
    }
    
    /* // init1 starts <to initialise helper data> */
    private static void init1() {
        // get all objects from the org
        gd = Schema.getGlobalDescribe();
        
        // to store objects and their prefixes
        keyPrefixMap = new Map<String, String>{};
        
        //get the object prefix in IDs
        keyPrefixSet = gd.keySet();
        
        // fill up the prefixes map
        for(String sObj : keyPrefixSet) {
            Schema.DescribeSObjectResult r =  gd.get(sObj).getDescribe();
            String tempName = r.getName();
            String tempPrefix = r.getKeyPrefix();
            keyPrefixMap.put(tempPrefix, tempName);
        }
    }
    /* // init1 ends */

    /* // init2 starts <to initialise helper data> */
    private static void init2() {
        
        // Create a blank list
        allSubordinates = new List<User>();
        
        // Get role to users mapping in a map with key as role id
        roleUsersMap = new Map<Id, UserRole>([select Id, Name, parentRoleId, (select id, name from users) from UserRole order by parentRoleId]);
        
        // populate parent role - child roles map
        parentChildRoleMap = new Map <Id, List<UserRole>>();        
        for (UserRole r : roleUsersMap.values()) {
            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);
            }
        }
    } 
    /* // init2 ends */

    /* // public method to get the starting node of the RoleTree along with user list */
    public static RoleNodeWrapper getRootNodeOfUserTree (Id userOrRoleId) {
        return createNode(userOrRoleId);
    }
    
    /* // createNode starts */
    private static RoleNodeWrapper createNode(Id objId) {
        RoleNodeWrapper n = new RoleNodeWrapper();
        Id roleId;
        if (isRole(objId)) {
            roleId = objId;
            if (!roleUsersMap.get(roleId).Users.isEmpty()) {
                n.myUsers = roleUsersMap.get(roleId).Users;
                allSubordinates.addAll(n.myUsers);
                n.hasUsers = true;
            }
        }
        else {
            List<User> tempUsrList = new List<User>();
            User tempUser = [Select Id, Name, UserRoleId from User where Id =: objId];
            tempUsrList.add(tempUser);
            n.myUsers = tempUsrList;
            roleId = tempUser.UserRoleId;
        }
        n.myRoleId = roleId;
        n.myRoleName = roleUsersMap.get(roleId).Name;
        n.myParentRoleId = roleUsersMap.get(roleId).ParentRoleId;

        if (parentChildRoleMap.containsKey(roleId)){
            n.hasChildren = true;
            n.isLeafNode = false;
            List<RoleNodeWrapper> lst = new List<RoleNodeWrapper>();
            for (UserRole r : parentChildRoleMap.get(roleId)) {
                lst.add(createNode(r.Id));
            }           
            n.myChildNodes = lst;
        }
        else {
            n.isLeafNode = true;
            n.hasChildren = false;
        }
        return n;
    }
    
    public static List<User> getAllSubordinates(Id userId){
        createNode(userId);
        return allSubordinates;
    }
    
    public static String getTreeJSON(Id userOrRoleId) {
        gen = JSON.createGenerator(true);
        RoleNodeWrapper node = createNode(userOrRoleId);
        gen.writeStartArray();
            convertNodeToJSON(node);
        gen.writeEndArray();
        return gen.getAsString();
    }
    
    private static void convertNodeToJSON(RoleNodeWrapper objRNW){
        gen.writeStartObject();
            gen.writeStringField('title', objRNW.myRoleName);
            gen.writeStringField('key', objRNW.myRoleId);
            gen.writeBooleanField('unselectable', false);
            gen.writeBooleanField('expand', true);
            gen.writeBooleanField('isFolder', true);
            if (objRNW.hasUsers || objRNW.hasChildren)
            {
                gen.writeFieldName('children');
                gen.writeStartArray();
                    if (objRNW.hasUsers)
                    {
                        for (User u : objRNW.myUsers)
                        {
                            gen.writeStartObject();
                                gen.writeStringField('title', u.Name);
                                gen.writeStringField('key', u.Id);
                            gen.WriteEndObject();
                        }
                    }
                    if (objRNW.hasChildren)
                    {
                        for (RoleNodeWrapper r : objRNW.myChildNodes)
                        {
                            convertNodeToJSON(r);
                        }
                    }
                gen.writeEndArray();
            }
        gen.writeEndObject();
    }
    
    /* // general utility function to get the SObjectType of the Id passed as the argument, to be used in conjunction with */ 
    public static String getSObjectTypeById(Id objectId) {
        String tPrefix = objectId;
        tPrefix = tPrefix.subString(0,3);
        
        //get the object type now
        String objectType = keyPrefixMap.get(tPrefix);
        return objectType;
    }
    /* // utility function getSObjectTypeById ends */
    
    /* // check the object type of objId using the utility function getSObjectTypeById and return 'true' if it's of Role type */
    public static Boolean isRole (Id objId) {
        if (getSObjectTypeById(objId) == String.valueOf(UserRole.sObjectType)) {
            return true;
        }
        else if (getSObjectTypeById(objId) == String.valueOf(User.sObjectType)) {
            return false;
        } 
        return false;
    }
    /* // isRole ends */
    
    public class RoleNodeWrapper {
    
        // Role info properties - begin
        public String myRoleName {get; set;}
        
        public Id myRoleId {get; set;}
        
        public String myParentRoleId {get; set;}
        // Role info properties - end
        
        
        // Node children identifier properties - begin
        public Boolean hasChildren {get; set;}
        
        public Boolean isLeafNode {get; set;}
        
        public Boolean hasUsers {get; set;}
        // Node children identifier properties - end
        
        
        // Node children properties - begin
        public List<User> myUsers {get; set;}
    
        public List<RoleNodeWrapper> myChildNodes {get; set;}
        // Node children properties - end   
        
        public RoleNodeWrapper(){
            hasUsers = false;
            hasChildren = false;
        }
    }
 }

2. TreeView (Visualforce component): Dynatree based reusable VF component that exposes input parameters like

a. roleOrUserId - required string type input attribute
b. selectable - boolean attribute to indicate whether you want to display checkboxes against nodes in the tree for user selection
c. JsonData - optional string type input attribute, if supplied to the component ignores the "roleOrUserId" attribute and displays the tree structure for the input JSON string
d. value - a string type output attribute which returns the IDs/Keys of the selected nodes in the CSV format, which can then be utilised by the page controller

<apex:component controller="TreeViewController">
    <apex:attribute name="roleOrUserId" required="true" type="String" assignTo="{!roleOrUserId}" description="Enter Role or User Id to build the hierarchy. Pass null if you are passing JSON data as a parameter" />
    <apex:attribute name="selectable" type="Boolean" assignTo="{!selectable}" description="Do you want nodes to be selectable?" />
    <apex:attribute name="value" type="String" description="IDs of selected Nodes in CSV format" />
    <apex:attribute name="JsonData" type="String" assignTo="{!JsonData}" description="JSON input for the tree component" />
    <apex:inputHidden id="selectedKeys" value="{!value}" />
    <apex:includeScript value="{!URLFOR($Resource.DynaTree, 'jquery/jquery.js' )}" />
    <apex:includeScript value="{!URLFOR($Resource.DynaTree, 'jquery/jquery-ui.custom.js' )}" />
    <apex:includeScript value="{!URLFOR($Resource.DynaTree, 'jquery/jquery.cookie.js' )}" />
    <apex:includeScript value="{!URLFOR($Resource.DynaTree, 'src/jquery.dynatree.js' )}" />
    
    <apex:stylesheet value="{!URLFOR($Resource.DynaTree, 'src/skin/ui.dynatree.css')}" />

    <!-- Add code to initialize the tree when the document is loaded: -->
    <script type="text/javascript">
    $(function(){
        // Attach the dynatree widget to an existing <div id="tree"> element
        // and pass the tree options as an argument to the dynatree() function:
        $("#tree").dynatree({
            onActivate: function(node) {
                // A DynaTreeNode object is passed to the activation handler
                // Note: we also get this event, if persistence is on, and the page is reloaded.
                //alert("You activated " + node.data.key);
            },
            persist: false,
            checkbox: {!selectable},
            generateIds: false,
            classNames: {
                checkbox: "dynatree-checkbox",
                expanded: "dynatree-expanded"
            },
            selectMode: 3,
            children: {!JsonString},
            onSelect: function(select, node) {
                // Get a list of all selected nodes, and convert to a key array:
                var selKeys = $.map(node.tree.getSelectedNodes(), function(node){
                    return node.data.key;
                });
                jQuery(document.getElementById("{!$Component.selectedKeys}")).val(selKeys.join(", "));
                
                // Get a list of all selected TOP nodes
                var selRootNodes = node.tree.getSelectedNodes(true);
                // ... and convert to a key array:
                var selRootKeys = $.map(selRootNodes, function(node){
                    return node.data.key;
                });
            },
        });
    });
    </script>
    
    <!-- Add a <div> element where the tree should appear: -->
    <div id="tree"> </div>

</apex:component>

You can see a working demo of the functionality here: http://treeview-developer-edition.ap1.force.com/

The code is available as unmanaged package (https://login.salesforce.com/packaging/installPackage.apexp?p0=04t90000000LlqQ) if you want to use it in your org. The code has been written assuming positive use cases and exceptional situations have not much been handled. It is advised to review and tweak the code before you use it in your org.

50 comments:

  1. Excellent post Ankit! From the RoleUtil code, it seems that in the tree that you have published, folder nodes represent user roles whereas file nodes represent users. Is that correct?

    ReplyDelete
  2. Great post! One bit of feedback - I would move

    gen = JSON.createGenerator(true);

    to the static initializer. As things are right now, you may be leaking JSONGenerators.

    ReplyDelete
  3. sir, is it possible to add command link in dyna tree view. as standard role tree Hierarchy have edit, delete links for editing and deleting roles???

    ReplyDelete
  4. sir is it possible to show image of contact in formula field in contact detail page(image is stored in notes and attachments) whenever image is saved /changed/multiple image(only customisation - no coding) is stored(takes only first image)

    ReplyDelete
  5. It is working in Chorme but the same is not working in IE; some javascript erros any idea?

    ReplyDelete
  6. Firgured out what is the issue, you have an extra , (comma) at the end of the fucntion onSelect: function(select, node) by removing this it works in IE as well. Thanks!

    ReplyDelete
  7. Hey Ankit,

    Is it possible to Edit or Delete a role through a visualforce page using this approach?

    ReplyDelete
  8. Its nice blog with lot of information thanks for sharing keep doing it

    dot net training in chennai

    ReplyDelete
  9. Thanks for share the innovative message its very useful for us

    salesforce training in chennai

    ReplyDelete
  10. Pega Training in Chennai
    This post is really nice and informative. The explanation given is really comprehensive and informative..

    ReplyDelete
  11. Latest Govt Bank Railway Jobs Notification 2016


    First i would like greet author, thanks for providing valuable information...................

    ReplyDelete
  12. hai have a good day.....
    i think this is useful for all of us..i am really enjoying when i reading.thanks for updating this informative article..
    i am waiting for your upcoming article..i hope it will be come soon as possible...http://sonymobileservicecenterinchennai.in/

    ReplyDelete
  13. Hi Ankit ,
    This application is great, how ever i am not getting check boxes only tree renders can u help me out please?

    ReplyDelete
  14. Excellent information with unique content and it is very useful to know about the information based on blogs.
    Informatica Training In Chennai
    Hadoop Training In Chennai
    Oracle Training In Chennai
    SAS Training In Chennai

    ReplyDelete
  15. Great post and informative blog.it was awesome to read, thanks for sharing this great content to my vision.
    Informatica Training In Chennai
    Hadoop Training In Chennai
    Oracle Training In Chennai
    SAS Training In Chennai

    ReplyDelete
  16. Really interesting content which is unique which provided me the required information.
    Dot Net Training in Chennai | .Net training in Chennai | FITA Training | FITA Velachery .

    ReplyDelete
  17. This info is very useful to me.I'm pretty new to salesforce. I have this query,I'm not able to save the VF page it shows that needs to be included, but when I include that it throws me an error stating that can't be used within .

    ReplyDelete
  18. I need to display the same based on profile I have a custom object for the same which contains parent and child data with profiles. however, while using this getting error "invalid id" though I have replaced the id mentioned in VF component. Could you please help me to make it work.

    ReplyDelete
  19. its really great information Thank you sir And keep it up More Post.the best rac training in chennai.the best rac training in chennai

    ReplyDelete
  20. 100%job training.the best TERADATA training in chennai.visit:the best teradata training in chennai

    ReplyDelete
  21. 100%JOB TRAINING IN CHENNAI.THE BEST INFORMIX TRAINING IN CHENNAI.the best informix training in chennai

    ReplyDelete
  22. This comment has been removed by the author.

    ReplyDelete
  23. check this one. no jquery or java script. simple code in apex and vf
    https://www.youtube.com/watch?v=IJdP3fziTS0

    ReplyDelete
  24. Greens Technology's the leading software Training & placement centre Chennai & ( Adyar)
    unix training in chennai

    ReplyDelete
  25. i gain the knowledge of Java programs easy to add functionalities play online games, chating with others and industry oriented coaching available from greens technology chennai in Adyar may visit.Core java training In Chennai

    ReplyDelete
  26. Shree Ram Techno Solutions Provides CCTV Camera, Security Camera, Wireless Security, Attendance System, Access Control System, DVR, NVR, Spy Camera, Fire Alarm, Security Alarm, PCI, IP Network Camera, Dome Camera, IR Camera, CCTV, Camera Price, HIKVISION, SCATI, Time Machine

    CCTV CAmera in jaipur at Rajasthan
    Home security system in jaipur
    Wireless Home Security System in jaipur
    Realtime attendance machine in jaipur
    cctv camera dealer in jaipur
    Hikvision DVR in jaipur at Rajasthan
    security system solutions in jaipur

    ReplyDelete
  27. hi, is there any chance of getting view state error, if there are large number of Role hierarchy record..if so how to optimize that
    ...
    please suggest

    ReplyDelete
  28. Freelance Best Makeup & Hair Artist in Jaipur with huge experience and Specialization in Bridal and Wedding Makeup,Celebrity Makeup,Professional Makeup,Creative Makeup,Bollywood Makeup and Character Makeup in Delhi,Jaipur,Rajasthan. Natural Makeup that allows your skin to breath with a radiant glow and remains flawless throughout your special day.


    Best Makeup and Hairstyle in jaipur
    Fiza Makeup Academy in jaipur
    Best bridal makeup artist in jaipur(bollywood makeup,creative makeup,Airbrush makeup,character makeup)
    Make up and Hair kit
    Professional makeup artist course in jaipur
    Makeup and hairstyle tips
    Makeup and hair Images
    Makeup and hair tutorials
    Makeup and hair contract


    ReplyDelete
  29. Wow, that was a nice article on Displaying Role Hierarchy on Visualforce Page and I have used as a tutorial and I have really learned a lot within a short period of time and I am happy that I landed on this page and found this article. I am looking forward to reading more articles from this site as I collect Tips for Writing a Dissertation Paper.

    ReplyDelete
  30. A pioneer Institute owned by industry professionals to impart vibrant, innovative and global education in the field of Hospitality to bridge the gap of 40 lakh job vacancies in the Hospitality sector. The Institute is contributing to the creation of knowledge and offer quality program to equip students with skills to face the global market concerted effort by dedicated faculties, providing best learning environment in fulfilling the ambition to become a Leading Institute in India.

    cha jaipur
    management college in jaipur
    management of hospitality administration jaipur
    cha management jaipur
    Best hotel college in jaipur
    Best management college in jaipur
    College of Hospitality Administration, Jaipur

    ReplyDelete