Sunday, January 15, 2017

Customizable Visualforce Component to display Hierarchy Relationship between records for any Object (Account Hierarchy, Case Hierarchy etc.)

It is very common use case to display hierarchy relation between records in tabular form (for example account hierarchy, case hierarchy etc.) In order to achieve this, I have created a reusable visualforce component for this purpose which can be customize as per requirement to display hierarchy in table tree grid.



Below are inputs required for component:
  1. Specify the object name for which you want to display hierarchy.
  2. Specify the parent field API name (used for self relationship).
  3. Specify the API names of fields separated by comma which you want to display in table tree grid.
  4. Specify the columns labels separated by comma (make sure sequence is same as that of API fields name )for table tree grid.
  5. Specify the field API name which will display as hyperlink for record detail page.

You can create new VF page and include this component and specify above inputs and component will display the hierarchy of record starting from top most parent. 

Below is visualforce component and apex class code:
//--------------------------------------------------------------
//Author- Sunil Kumar (sunil02kumar@gmail.com)
//Date - 15 Jan, 2017
//Purpose - Controller for ComponentHierarchy to generate Data for record Hierarchy
//-----------------------------------------------------------
public class hierarchyComponentController {
public string recordid{get;set;}
public String objectAPIName{get;set;}
public String parentFieldAPIName{get;set;}
public String columnList{get;set;}
public String ColumnsLabelList{get;set;}
public String fieldAPINameForRecordLink{get;set;}
public hierarchyComponentController(){
recordid= ApexPages.currentPage().getParameters().get('id');
}
public PageReference GenerateHierarchy(){
if(recordid !=null && recordid !=''){
generateQueryString();
generateHierarchyData();
}else{
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO,'Please specify the record id in URL for id parameter.'));
}
return null;
}
//Code to generate account hierarchy--start-------------------
//Method to find Find the top most element in Heirarchy
public string queryStringForUltimateParent;
public string queryStringForHierarchy;
public List<String> fieldAPINameList{get;set;}
public List<String> ColumnsLabelListForUI{get;set;}
public void generateQueryString(){
system.debug('***parentFieldAPIName:'+parentFieldAPIName);
system.debug('***columnList:'+columnList);
system.debug('***objectAPIName:'+objectAPIName);
queryStringForUltimateParent = 'Select id, ';
queryStringForHierarchy = 'Select id, ';
string fieldQuery = '';
fieldAPINameList = new List<String>();
ColumnsLabelListForUI = new List<String>();
if(ColumnsLabelList!=null){
List<string> columnlabels= ColumnsLabelList.trim().split(',');
system.debug('***********columnlabels:'+columnlabels);
if(columnlabels.size() > 0 ){
for(string ss : columnlabels){
if(ss != null && ss != ''){
ColumnsLabelListForUI.add(ss.trim());
}
}
}
}
if(parentFieldAPIName!=null){
queryStringForUltimateParent = queryStringForUltimateParent + parentFieldAPIName ;
queryStringForHierarchy = queryStringForHierarchy + parentFieldAPIName ;
}
if(columnList!=null){
List<string> filedNames = columnList.trim().split(',');
system.debug('***********filedNames:'+filedNames);
if(filedNames.size() > 0 ){
if(fieldAPINameForRecordLink==null || fieldAPINameForRecordLink == ''){
fieldAPINameForRecordLink=filedNames[0].trim();
}
for(string ss : filedNames){
if(ss != null && ss != ''){
if(ss != parentFieldAPIName){
fieldQuery = fieldQuery + ' , ' + ss.trim();
}
fieldAPINameList.add(ss.trim());
}
}
}
queryStringForHierarchy = queryStringForHierarchy + fieldQuery;
}
if(objectAPIName!=null){
queryStringForUltimateParent = queryStringForUltimateParent + ' from ' + objectAPIName ;
queryStringForHierarchy = queryStringForHierarchy + ' from ' + objectAPIName ;
}if(fieldAPINameList.size() != ColumnsLabelListForUI.size()){
ColumnsLabelListForUI = new List<String>();
ColumnsLabelListForUI = fieldAPINameList;
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO,'Mismatch between number of fields and labels. So System generates table with column label as field API name'));
}
system.debug('***queryStringForHierarchy:'+queryStringForHierarchy);
system.debug('***ColumnsLabelListForUI:'+ColumnsLabelListForUI);
system.debug('***fieldAPINameList:'+fieldAPINameList);
}
public String GetUltimateParentId( String queryString ){
string objId = recordid;
queryString = queryString + ' where id =:objId Limit 1';
if(queryString!=null){
Boolean top = false;
if(Limits.getLimitQueries()-Limits.getQueries()>0){
while ( !top ) {
try{
system.debug('******Limits.getLimitQueries():'+Limits.getLimitQueries());
system.debug('******Limits.getQueries():'+Limits.getQueries());
system.debug('***queryString:'+queryString);
sObject rec = database.query(queryString);
if ( rec.get(parentFieldAPIName) != null ) {
objId = string.valueof(rec.get(parentFieldAPIName));
system.debug('***objId in GetUltimateParentId:'+objId);
}
else {
top = true;
}
}catch(exception ex){
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Something is wrong with either sobject or field API names. Please check and provide correct API names.'+ ex.getmessage()));
system.debug('****Exception while finding GetUltimateParentId.'+ ex.getmessage());
//top = true;
}
}
}else{
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Limit reached for number of SOQL Queries.'));
}
}
system.debug('***objId in GetUltimateParentId:'+objId);
return objId ;
}
public List<HierarchyWrapper> dataForHierarchy{get;set;}
public void generateHierarchyData(){
system.debug('***recordid:'+recordid);
system.debug('***queryStringForHierarchy:'+queryStringForHierarchy);
dataForHierarchy = new List<HierarchyWrapper>();
//section to generate query string for account ends--------
system.debug('*******current recordidid:' + recordid);
List<String> currentParent = new List<String>{};
Integer count = 0;
Integer level = 0;
Boolean endOfStructure = false;
if(GetUltimateParentId(queryStringForUltimateParent)!=null){
currentParent.add( GetUltimateParentId(queryStringForUltimateParent));
}else{
currentParent.add(recordid);
}
system.debug('**********ultimateParentId:'+ currentParent);
//Loop though all children
List<sobject> sbList = new List<sobject>{};
string queryStr = '';
while ( !endOfStructure ){
if( level == 0 ){
queryStr = queryStringForHierarchy + ' where id IN : CurrentParent ORDER BY '+ parentFieldAPIName +' Limit 9000';
}
else {
queryStr = queryStringForHierarchy + ' where ParentID IN : CurrentParent ORDER BY '+ parentFieldAPIName+ ' Limit 9000';
}
system.debug('*********queryStr for child records:'+queryStr);
if(queryStr != null && queryStr !=''){
try{
if(Limits.getLimitQueries()-Limits.getQueries()>0){
sbList = database.query(queryStr);
}else{
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Limit reached for number of SOQL Queries.'));
endOfStructure = true;
}
}catch(exception ex){
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Something is wrong with either sobject or field API names. Please check and provide correct API names.'+ex.getmessage()));
endOfStructure = true;
}
}
if( sbList.size() == 0 ){
endOfStructure = true;
}
else{
currentParent.clear();
for ( Integer i = 0 ; i < sbList.size(); i++ ){
//Change below
sobject sb = sbList[i];
HierarchyWrapper aw = new HierarchyWrapper();
aw.recordId =string.valueof(sb.get('id'));
if(sb.get('ParentId')!=null){
aw.idForNode = 'treegrid-'+ sb.get('id') +' treegrid-parent-'+ sb.get('ParentId');
}else{
aw.idForNode = 'treegrid-'+ sb.get('id');
}
for(String ss:FieldAPINameList){
try{
string colValue ='';
system.debug('******column API Name .'+ ss );
if(sb.get(ss)!=null){
colValue = string.valueof(sb.get(ss));
if(ss.trim().equalsignorecase(fieldAPINameForRecordLink.trim())){
aw.hyperlinkColumnValue = colValue;
}
}
system.debug('******colValue .'+ colValue );
aw.recordtDetails.add(colValue);
}catch(exception ex){
system.debug('*******Error in some Field API Name.'+ ex.getmessage());
}
}
currentParent.add(string.valueof(sb.get('id')) );
dataForHierarchy.add(aw);
}
}
level = level + 1;
}
system.debug('**********dataForHierarchy:'+dataForHierarchy);
}
public class HierarchyWrapper{
public string idForNode{get;set;}
public string recordId{get;set;}
public string hyperlinkColumnValue{get;set;}
public List<String> recordtDetails{get;set;}
HierarchyWrapper(){
idForNode= '';
recordId='';
hyperlinkColumnValue='';
recordtDetails= new List<String>();
}
}
//Code to generate hierarchy--end-------------------
}
<!--*************************************************************
//Author- Sunil Kumar (sunil02kumar@gmail.com)
//Date - 15 Jan, 2017
//Purpose - Controller for ComponentHierarchy to generate Data for record Hierarchy
***************************************-->
<apex:component controller="hierarchyComponentController" >
<head>
<!--for treegrid-->
<apex:includescript value="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js" / >
<apex:stylesheet value=" https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
<apex:stylesheet value=" https://cdnjs.cloudflare.com/ajax/libs/jquery-treegrid/0.2.0/css/jquery.treegrid.css" />
<apex:includescript value="https://cdnjs.cloudflare.com/ajax/libs/jquery-treegrid/0.2.0/js/jquery.treegrid.js" / >
<apex:includescript value="https://cdnjs.cloudflare.com/ajax/libs/jquery-treegrid/0.2.0/js/jquery.treegrid.bootstrap3.js" />
<!--treegrid resources end-->
<script>
var sk= $.noConflict();
sk(document).ready(function() {
sk('#loadingImage').show();
sk('#HierarchyDiv').hide();
FindHierarchyDetails();
});
function displayHierarchy(){
sk('#HierarchyDiv').show();
sk('#loadingImage').hide();
}
</script>
</head>
<body>
<apex:attribute Name="sObjectAPIName" type="String" assignTo="{!objectAPIName}" description="Specify the API name of object" required="true"/>
<apex:attribute Name="sParentfieldAPIName" type="String" assignTo="{!parentFieldAPIName}" description="Specify the API name of field" required="true" />
<apex:attribute Name="ColumnsToDispaly" type="String" assignTo="{!columnList}" description="Specify the API name of field separated by comma which you want to display in grid" required="true" />
<apex:attribute Name="ColumnsLabels" type="String" assignTo="{!ColumnsLabelList}" description="Specify the Label for columns separated by comma in same sequence as that of Column API Name" required="true" />
<apex:attribute Name="RecordLinkfieldAPIName" type="String" assignTo="{!fieldAPINameForRecordLink}" description="Specify the API name of field" />
<apex:form id="f1">
<script>
sk('.tree-3').treegrid({
expanderExpandedClass: 'glyphicon glyphicon-minus',
expanderCollapsedClass: 'glyphicon glyphicon-plus'
});
</script>
<apex:pageMessages />
<apex:actionFunction name="FindHierarchyDetails" action="{!GenerateHierarchy}" id="gsb" reRender="f1" oncomplete="displayHierarchy()"/>
<div class="container">
<div class="masthead">
<h3 class="text-muted">Hierarchy View</h3>
</div>
<!-- Content Section start -->
<div class="jumbotron tab-content" style="margin-top: 10px;">
<div id="allcontentsection" class="tab-pane fade active in">
<div id="loadingImage">
<img src="/img/loading.gif" />Loading Hierarchy....
</div>
<!-- Div for hierarchy-->
<div id="HierarchyDiv">
<apex:outputtext value="Total child records from top most parent:={!DataForHierarchy.size}" rendered="{!NOT(ISNULL(DataForHierarchy))}"/>
<table class="table tree-3 table-bordered table-striped table-condensed">
<tr>
<apex:repeat value="{!ColumnsLabelListForUI}" var="label">
<td><b><apex:outputtext value="{!label}" /></b> </td>
</apex:repeat>
</tr>
<apex:repeat value="{!DataForHierarchy}" var="obj">
<tr class="{!obj.idForNode}">
<apex:repeat value="{!obj.recordtDetails}" var="ColValue">
<td >
<apex:outputLink value="/{!obj.recordId}" rendered="{!if(obj.hyperlinkColumnValue==ColValue, true, false )}">{!ColValue}</apex:outputLink>
<apex:outputtext value="{!ColValue}" rendered="{!if(obj.hyperlinkColumnValue=ColValue, false, true)}"/>
</td>
</apex:repeat>
</tr>
</apex:repeat>
</table>
</div>
</div>
</div>
<!-- Content Section end -->
<!-- Footer Section start -->
<footer class="footer">
<p>Developed by: Sunil Kumar (sunil02kumar@gmail.com)</p>
</footer>
<!-- Footer Section start -->
</div>
</apex:form>
</body>
</apex:component>


For example to generate the account hierarchy, create a VF page with code :
<apex:page controller="hierarchyComponentController">
<c:hierarchyComponent sObjectAPIName="Account" sParentfieldAPIName="ParentId" ColumnsToDispaly="Name, Type, Industry" RecordLinkfieldAPIName="Name" ColumnsLabels="Account Name, Type, Industry"/>
</apex:page>

Note: Don't forget to append record Id on visualforce page url for which you want to display hierarchy. Below is sample URL:
https://xxxxxxxxx.visual.force.com/apex/accHierarchy?id=0019000001NqoXZ

Hope this will help...

This can be used as base code and can be customize as per your requirements.

Looking forward for everyone's comment and feedback.




Refer Below Links for Salesforce Interview Questions

14 comments:

  1. Sunil, if we want to do for 5 level hierarchy, will this work ?

    ReplyDelete
    Replies
    1. Yes. This will work. I have tested upto 10 level deep.

      Delete
  2. Hi sunil,
    Thank you very much for the code,it really helpful for me,but when i try it in my org i am getting this error

    Error: is required and must be the outermost tag in the markup at line 1 column 1

    where is c:hierarchyComponent code?can you please suggest me

    ReplyDelete
    Replies
    1. Hi sunil,
      I got the solution,I have done dumb mistake.
      Thanks

      Delete
  3. Hi Sunil,

    Instead of passing single id in url,I want to display all records of object and when i click a record it should display hirarchy of that record,Can we do it with the code you provided.Can you please help in this.Its urgent

    Thanks,
    nandeesh.

    ReplyDelete
  4. I sunil thanks for your blog. I want to display parent releted child till nth level but its showing all the children at the third level .
    Can you please help me on that.

    ReplyDelete
  5. What would it take to make this display in a Lightning Component?

    ReplyDelete
    Replies
    1. Hi Thaddeus,

      You can refer below blog to implement this in Lightning.

      http://www.sfdcstuff.com/2017/10/lightning-tree-generic-lightning.html

      Delete
  6. If you think your girlfriend is cheating on you, then this is the e-book for you. Learn all of the techniques to catch her at her own game lie detectors

    ReplyDelete
  7. These are common but important things we live by each day and we cannot just simply ignore that our partners may have interests different to that of our own. Dealing with it is never easy especially if you are not use to it.  what to talk to girls about

    ReplyDelete
  8. Knows more about relationships? Then give a helping hand to your young ones. Let's grow the world knowledge. Head over to Thinkle Reletionship and help a seeker please.

    ReplyDelete
  9. I'm having an issue where child records are appending at the bottom and not nested under the parent. I have top tier, company, & company branch hierarchy. Companies are accurately listed under Top Tier, but the branches are not listed under their appropriate company.

    ReplyDelete
  10. i love reading this article so beautiful!!great job! Relatietherapie Zaandam

    ReplyDelete
  11. This is awesome, but the spacing is all off. Can anyone help me with this?

    ReplyDelete