Showing posts with label sites. Show all posts
Showing posts with label sites. Show all posts

Tuesday, April 26, 2011

Google User Org-Chart with Salesforce

Cloudspokes.com had a challenge to incorporate Google's user hierarchy chart in Salesforce on the user object. And even though I didn't win (totally should have lol) I thought I would share my version of the challenge.  
In mine, the Hierarchy chart is based off of the Manager Id of each User, instead of going into Roles and all that mess. But when I set it up for all users, THe chart produced was very long and made the page scroll, to fix that issue, I made it so it grouped by department, and by default show the current User's Department. Then I added a fixed width on the page so it wouldn't scroll, except inside the frame, and if the department drop-down is on the page it would always show on top right regardless of how long the chart is that gets created. Since not all managers where in the same department as their subordinates, I had to re-query for any managers that weren't in the list the of the users returned, other wise it showed as only an ID above the user, which looked bad lol.
I have set the page up to take a few different params(see below) which control options for the page, including hiding/showing a User table, a select list for departments, and the hiding/showing of the sidebar and header. 
Additional options lie in the JavaScript, there you have the ability to allow people to click on a user and see additional info on them(works/looks really good on iPad). Or control the Google chart options like allow HTML and the collapse feature. To see the  'onclick additional info' in action simply click once on a users name and it will pop up with a small modal of that info.

When trying to make this work, i found that apex and Google do not play nice, especially with re-render, so i had to make a lot of custom JavaScript to make it all work correctly, and i could not use Apex:output or have the JS in the re-render, otherwise it would spit out all the code to the page on re-render. 

Here is the JS
/*SALESFORCEGIRL ORGCHAT 2011*/

/*CONTROLS THE LOADING GIF*/
function Loading(b){
     if(b){
          $('.loadingIcon').removeClass('hideIt');
     }else{
          $('.loadingIcon').addClass('hideIt');
     }
}
/*PUTS THE USER LIST IN THE RIGHT FORMAT WITH OPTIONS FOR GOOGLE TO READ*/
function getUserList(userOptions, CurrentUserId, dptLabel,phLabel,rxtLabel,emLabel) {
     var users = [];
     var UserString = '';
     var uId = '';
     var uName = '';
     var uManId = '';
     var uDept = '';
     var uPhone = '';
     var uExt = '';
     var uEmail = '';                 
     $('#hiddenUserData tr.dataRow').each(function(){
          uId = $(this).find('td:eq(0)').text();
          uManId = $(this).find('td:eq(1)').text();
          uName = $(this).find('td:eq(2)').text();
          uDept = $(this).find('td:eq(3)').text();
          uPhone = $(this).find('td:eq(4)').text();
          uExt = $(this).find('td:eq(5)').text();
          uEmail = $(this).find('td:eq(6)').text();
      
          UserString = '<div class="UserInfo" onClick="ShowMoreInfo(this)">'+uName+'<br /><div class="Title" >'+uDept+'</div><br />';
      
          if (userOptions.showInfo.department || userOptions.showInfo.phone || userOptions.showInfo.ext || userOptions.showInfo.email) {
               UserString += '<div title="Click to hide" class="additionalInfo hideMe"><table><tbody><tr><td colspan="2">';
           
               UserString += '<div class="HeaderTitle">'+uName+'</div></td></tr>';
           
               if (userOptions.showInfo.department) {
                    UserString += '<tr><th>'+dptLabel+'</th><td>'+uDept+'</td></tr>';
               }
           
               if (userOptions.showInfo.phone) {
                    UserString += '<tr><th>'+phLabel+'</th><td>'+uPhone+'</td></tr>';
               }
           
               if (userOptions.showInfo.ext) {
                    UserString += '<tr><th>'+rxtLabel+'</th><td>'+uExt+'</td></tr>';
               }
           
               if (userOptions.showInfo.email) {
                    UserString += '<tr><th>'+emLabel+'</th><td>'+uEmail+'</td></tr>';
               }
           
               UserString += '</tbody></table></div>';
          }
          UserString += '</div>';
 
          users.push([{v: uId, f: UserString}, uManId, 'Click for additional info']);
     });                     
     return users;
}
/*USED TO TRIGGER THE ADDITIONAL INFO POP UP */
function Loading(b){
     if(b){
          $('.loadingIcon').removeClass('hideIt');
     }else{
          $('.loadingIcon').addClass('hideIt');
     }
}
/*USED TO SHOW MORE INFO IF USER IS CLICKED*/
function ShowMoreInfo(t){
     var item = $(t).find('.additionalInfo');
     if($(item).hasClass('hideMe')){
          $('.additionalInfo').addClass('hideMe');
          $(item).removeClass('hideMe');
     }else{
          $(item).addClass('hideMe');
     }
}
/*SETTING UP THE GOOGLE CHART*/
function initializeOrgChart(){
     google.load('visualization', '1', {packages:['orgchart']});
}
/*SETTING UP THE GOOGLE CHART*/
function getUserDataTable(userList){
     var dt = new google.visualization.DataTable();
       dt.addColumn('string', 'Name');
       dt.addColumn('string', 'Manager');
       dt.addColumn('string', 'ToolTip');
       dt.addRows(userList);
       return dt;
}
/*SETTING UP THE GOOGLE CHART*/
function getOrgChart(container,options,data){
     var chart = new google.visualization.OrgChart(container);
       chart.draw(data, options);
     return chart;
}
/*SETTING UP THE GOOGLE CHART*/
function drawOrgChart(container,options) {         
     /* DEFAULT OPTIONS STRUCTURE.
     var options = {
          users: null,                    
          chartOptions: {
               allowHtml: true,
               allowCollapse:true
          }
     };*/
       var data  = getUserDataTable(options.users);
       var chart = getOrgChart(container,options.chartOptions,data);
}

Then on the page:
<apex:page controller="sfgOrgChart" title="Org Chart" showHeader="{!bShowHeader}" sidebar="{!bShowSidebar}" standardstylesheets="false" action="{!SwitchList}">
<html>
    <head>
        <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE8" />
        <apex:stylesheet value="{!URLFOR($Resource.OrgChart, '/style.css')}"/>
       
        <script type="text/javascript" language="javascript" src="{!URLFOR($Resource.OrgChart, '/js/jquery-1.4.2.min.js')}" ></script>
        <script type="text/javascript" language="javascript" src="{!URLFOR($Resource.OrgChart, '/js/jquery.tablesorter.min.js')}" ></script>
        <script type="text/javascript" language="javascript" src="{!URLFOR($Resource.OrgChart, '/js/sfgOrgChart.js')}" ></script>
        <script type='text/javascript' src='https://www.google.com/jsapi'></script>
    </head>
    <body>
        <apex:form id="form">
            <apex:outputpanel id="pageWrap">
                <apex:outputpanel id="ChooseDepartmentWrap" rendered="{!bShowDepartmentselectList}">
                    <div class="clear"></div>
                    <br />
                    <div class="floatL">
                        <apex:outputpanel rendered="{!NOT(ISBLANK(sDepartment))}">
                            <div class="HeaderTitle">Viewing Department:&nbsp;<apex:outputtext value="{!IF(bIsDept,sDepartment,'All Departments')}" /></div>
                        </apex:outputpanel>
                    </div>
                    <div class="floatR">
                        <apex:outputpanel rendered="{!bShowDepartmentSelectList}">
                            <strong>{!$ObjectType.User.Fields.Department.Label}:</strong>
                            <apex:selectList id="department" size="1" value="{!sDepartment}" styleClass="deptSelect"> 
                                <apex:selectOptions value="{!lstDepartmentOptions}" />
                                <apex:actionSupport event="onchange" onsubmit="Loading(true);$('#chart_div').html('');" action="{!SwitchList}" rerender="pageWrap" id="actionSupportForDeptartment" oncomplete="drawOrgChart($('#chart_div')[0],Go('{!$User.Id}'));ShowTableChart();Loading(false);" />
                            </apex:selectList>
                        </apex:outputpanel>
                    </div>
                    <!-- the loading icon -->
                    <div class="loadingIcon hideIt"><apex:image id="loadingImage" value="{!URLFOR($Resource.OrgChart, 'images/loader_24x26.gif')}" width="24" height="26"/></div>
                    <div class="clear"></div>
                </apex:outputpanel>
                <div id="chart_div"></div>
                <br />
                <div>
                    <table id="hiddenUserData" class="list tablesorter hideMe">
                        <thead class="rich-table-thead">
                        <tr class="headerRow">
                            <th colspan="1" scope="col">Id</th>
                            <th colspan="1" scope="col">Manager Id</th>
                            <th colspan="1" scope="col">Name</th>
                            <th colspan="1" scope="col">Department</th>
                            <th colspan="1" scope="col">Phone</th>
                            <th colspan="1" scope="col">Ext</th>
                            <th colspan="1" scope="col">Email</th>
                        </tr>
                        </thead>
                        <tbody>
                        <apex:repeat value="{!lstOfUsers}" var="u"><!-- Table to get the data from and to show if they wish it with sorting -->
                            <tr class="dataRow" id="{!u.id}">
                                <td>{!JSENCODE(u.id)}</td>
                                <td>{!JSENCODE(u.ManagerId)}</td>
                                <td>{!JSENCODE(u.name)}</td>
                                <td>{!JSENCODE(u.department)}</td>
                                <td>{!JSENCODE(u.phone)}</td>
                                <td>{!JSENCODE(u.extension)}</td>
                                <td>{!JSENCODE(u.email)}</td>
                            </tr>
                        </apex:repeat>
                    </tbody></table>
                </div>
        </apex:outputpanel>
        </apex:form>
        <script type="text/javascript" language="javascript">
          
            initializeOrgChart();  // LOAD API IMPORTANT TO DO THIS.          
            var ShowTable = {!bShowUserTable};
            var dptLabel = '{!$ObjectType.User.Fields.Department.Label}';
            var phLabel = '{!$ObjectType.User.Fields.Phone.Label}';
            var rxtLabel = '{!$ObjectType.User.Fields.Extension.Label}';
            var emLabel = '{!$ObjectType.User.Fields.Email.Label}';
          
            var winWidth = $(window).width() - 75 +'px !important;overflow-y:hidden;';
            var isiPad = navigator.userAgent.match(/iPad/i) != null;
            if(isiPad){
                winWidth = '100% !important;'
            }
              
            function Go(UserId){
              
                // THE ADDITIONAL INFO OPTIONS
               // IF ALL ARE FALSE WILL NOT SHOW ADDITIONAL INFO
                var userOptions = {
                      showInfo: {
                            department: true,      // show dept in additional info box
                            phone: true,           // show phone in additional info box
                            ext: true,             // show ext in additional info box
                            email: true            // show email in additional info box
            }
                };       
           //GOOGLE CHART OPTIONS
           var options = {
                 users: getUserList(userOptions,UserId, dptLabel,phLabel,rxtLabel,emLabel),
                 chartOptions: {               // default google chart options
                       allowHtml: true,        // allow html
                       allowCollapse:true      // allow collapsing 
                 }
           };                

            function ShowTableChart(){
                if(ShowTable){
                    $("table").tablesorter({
                        headers: {
                            0: {sorter: 'text'},
                            1: {sorter: 'text'},
                            2: {sorter: 'text'},
                            3: {sorter: 'text'},
                            4: {sorter: 'digit'},
                            5: {sorter: 'digit'},
                            6: {sorter: 'text'}
                        }
                    });
                    $('#hiddenUserData').removeClass('hideMe');
                }         
            }
            $(document).ready(function(){                 
                //DRAW CHART WITH DATA AND MAKE SURE IT WONT OVERFLOW
                $('#chart_div').attr('style','width:'+winWidth+' margin:5px auto; padding:10px 5px;');
                drawOrgChart($('#chart_div')[0],Go('{!$User.Id}'));
                $('.deptSelect').val('{!sDepartment}');
                //IF SHOW-TABLE START TABLE SORTER AND SHOW TABLE
                ShowTableChart();
            });
        </script>
    </body>
    </html>

</apex:page>
Example: If the dept param(URL_KEY_SHOWDEPT) is passed with URL_DEFAULT_VALUE_SHOWALL it will show all Users under all Departments, and if SHOW_SELECT_DEPT is set to true, then it will also show a Department select list so it is easy to change between departments. If you click on a User in the Chart it will display additional information on that user including: Name, Phone, Title, Email, Department, Extension. This can be turned off/on and/or you can set which info shows by customizing the JavaScript options. If all options are false, then the additional info will not show. 

NOTE: Department select list will only show if a param is passed and the SHOW_SELECT_DEPT is true...this is not the same for the other params.. see below

In the Common class there are a few variables that you can change to change the defaults of the page:
     
     // keys to look for in the url
     public static final string URL_KEY_SHOWDEPT = 'dept'; 
     public static final string URL_KEY_SHOWSIDEBAR = 'side';
     public static final string URL_KEY_SHOWHEADER = 'top';
     public static final string URL_KEY_SHOWUSER = 'user';

     // the show all param value
     public static final string URL_DEFAULT_VALUE_SHOWALL = 'all';

     // if true and param is passed will show select list 
     public static final boolean SHOW_SELECT_DEPT = true;

     // limit on the user Query
     public static final integer USER_SEARCH_LIMIT = 400;

     // show the header
     public static final boolean SHOW_HEAD = true;

     // show the side bar
     public static final boolean SHOW_SIDE = false;

     // show the table of users currently in the google charts
     public static final boolean SHOW_USER_LIST = true;

EXAMPLE URLs:

-shows Users by current users dept (nothing was passed so it defaults to Current Users Dept)
-shows header (nothing was passed so it defaults to SHOW_HEAD )
-hides sidebar (nothing was passed so it defaults to SHOW_SIDE )
-hides User Table (nothing was passed so it defaults to SHOW_USER_LIST )

URL_KEY_SHOWDEPT
-shows all Dept's and all Users(active) and also shows dept select list (if SHOW_SELECT_DEPT is true)

-shows Users in support dept and also shows dept select list (if SHOW_SELECT_DEPT is true)

-shows Users by current users dept and also shows dept select list (if SHOW_SELECT_DEPT is true)

URL_KEY_SHOWSIDEBAR 
-shows side bar

-hides side bar

URL_KEY_SHOWHEADER 
-shows header

-hides header

URL_KEY_SHOWSIDEBAR 
-shows User Table

-hides User Table



Questions? 

Twitter: @SalesForceGirl  or Facebook


Thursday, March 31, 2011

SalesForce Knowledge, a look Underneath

A little while ago I finished work on a support website that was housed on force.com sites and powered by knowledge, Salesforce's new KB solution. While it was fun to figure everything out, I thought I would share to help other avoid that same headache. 

Like all newly released software, it has some bugs and some issues that you will want to look out for, but overall, Salesforce did a great job on this one. Hopefully with some more updates it will reach its full potential and there will be less need for customization.

Creating articles in knowledge can be very troublesome, and the trouble can start sooner rather than later, so its important to carefully think out the best way to implement it based on the company's needs. Knowledge allows multiple 'DataCategories', which is what is used to help filter the search and organize the content. In the old Knowledge version(before 21.0) they didn't give much consideration to Language, so if multilingual support was needed, you would need to make sure you set theDataCategories appropriately before starting. Also if you wanted multilingual, you needed to figure out how to do the urls -> each doc requires a unique URL field(we ended up appending the lang code to the end). Otherwise you would have to go back thought and edit each doc you made to accommodate for that. With the newest release of knowledge (v21.0), it will have a required Language field that 'fixes' the need for it for so much for thought for such an otherwise simple option.


 The DataCategories themselves have their own issues, for one you can only edit / access them though the website, it is not available in the schema browser except though the article types, but only access based on that Artical type for filtering the query / search. On top of that, the interface to edit DataCategories in is, as far as i can tell, an attempt at improving the old edit pages with an ajax-ified 'high-tech' looking one. The page seems to be overloaded with onHovers, double-click to edit, ajax-rerenders and modals, to a point that loses usability, making it had to get at the data you need, thus rendering the page unusable. It does look nice though.

Creating different Articles Types for your knowledge base raises some more considerations that must be thought about before getting started. One nice thing is that you can create any number of Article Types, for example, my implementation has 9 including:
  • Alerts
  • Internal
  • Videos
  • Documents
  • FAQs
  • Release Notes
etc. On the surface, in the 'Article Management' tab, it gives you another 'ajax-ified' looking UI, that doesn't allow for easy sorting or viewing of the Articles. The table's columns are mostly sortable except by Type, they also allow you to filter by Data Category OR allow Queuing , making it impossible to manage anything more than a hundred articles. My Org has somewhere around  5000 articles across 3 languages and growing I might add. 


Unlike other sObject records in Salesforce, the Article Type obj has a required 'URL name' field that is also required to be unique. They also added attachments as a field type to the Article sObjects, but it can cause some unexpected errors saying that the DML limit has exceeded in certain use cases, and it had a few other quirks that just made working with them a headache. Since the out-of-the box fields on the sObject is minimal, its key to plan out each ArticalTypes fields carefully, and thoroughly to avoid having to back track later.
In making  an Article, the first thing to note is that Salesforce enforces the save draft, then publish principal, which would be good but without queues to sort the articles, it does little more than become a performance issue. Its also not possible to create and then publish Articles via apex, you can create a draft article, but not to publish or make it 'public'. 

The rich text editor that Salesforce uses is limiting since they are using a 'lite' version of another rich text editor called FCKeditor, making it necessary to compose the Article in HTML else where and copy / paste it into the editor. But as you would expect there is issue with this as well, since it was not designed for such use.

Looking at the Article in the schema browser you will notice that there are 5 sObject for every Artical Type, and each has a unique extension:

  • ArticalType__ka
  • ArticalType__kav
  • ArticalType__ViewStat
  • ArticalType__VoteStat
  • ArticalType__DataCategorySelection

The __ka sObject is the top level article, but it doesn't hold any of the actually article info like Body, Title, etc, that is housed in the __kav obj, which has access to the  __DataCategorySelection obj for searching. But if you want to see __ViewStat or __VoteStat, you need to go through the __ka object. This may seem like a pratical set up but the __ka object almost holds no value except to make it harder to transverse the layers, ideally it should be the __kav with everything underneath it. The __ViewStat and __VoteStat sObjects have a few drawbacks as well, if you were to have it on a public site, only users that login could rate Articles (__VoteStat), which means a Salesforce license for every potential user on the site. The __ViewStat however doesn't require log in, but with testing over time, we found that it would stop working periodically, and so wasn't reliable.

Example SOQL code with Data Categories:

List<FAQ__kav> f = Database.query('Select f.ranking__c,f.VideoUrl__c,MacVideoUrl__c,f.Localized_Title__c,f.KnowledgeArticleId From FAQ__kav f WHERE f.ranking__c != null and PublishStatus = \'online\' and IsVisibleInPkb = true WITH DATA CATEGORY Languages__c BELOW '+lang+'__c AND Products__c BELOW '+prod+'__c order by f.ranking__c DESC limit 10');

Example SOSL:

List<List<SObject>> qry = search.query('FIND \'access code\' IN ALL FIELDS Returning Documentation__kav(ID, knowledgearticleid, Title, Localized_Title__c, UrlName, LastPublishedDate WHERE PublishStatus = \'Online\' LIMIT 5),FAQ__kav(ID, knowledgearticleid, Title, Localized_Title__c, UrlName, LastPublishedDate WHERE PublishStatus = \'Online\' LIMIT 5) WITH DATA CATEGORY Products__c BELOW (lmipro2__c,lmifree__c) AND Languages__c BELOW (en__c,es__c)');

Example in accessing Data Categories:

               for (FAQ__kav f : faqList)
               {
                        FAQ__DataCategorySelection[] dc =  f.DataCategorySelections;
                        for(FAQ__DataCategorySelection d : dc)
                         {
                              if(d.DataCategoryGroupName == 'products')
                              {
                                    if(GlobalHelper.CheckForNotNull(dataCProd))
                                    {
                                          dataCProd = d.DataCategoryName;
                                    }else{
                                          dataCProd += ', '+ d.DataCategoryName;
                                    }
                              }
                              if(d.DataCategoryGroupName == 'languages')
                              {
                                    if(GlobalHelper.CheckForNotNull(dataCLang))
                                    {
                                          dataCLang = d.DataCategoryName;
                                    }else{
                                          dataCLang += ', '+ d.DataCategoryName;
                                    }
                              }
                        }
                  }

Again this was not meant to 'Bash' salesforce, rather it is meant to be a guide around the mishaps that I encountered when doing my implementation. But on a side note, because some of the issues are too big to ignore, I am now tasked with re-creating the whole site in .Net :-P 

Questions? 

Twitter: @SalesForceGirl  or Facebook