Showing posts with label support. Show all posts
Showing posts with label support. Show all posts

Friday, April 1, 2011

Knowledge's new multi-lingual support: A brief look

In my last post I briefly described that in the Spring'11 release Salesforce updated knowledge with new multi-lingual support. While it is good in theory, in practice it presents some problems that are hard to work with.

One of the issues of the old knowledge version is in mass importing docs that may have changed or updated. This is due to the required unique URL Name field, and there being no way to update, only add as 'new'. To get around this in the old version, our edit team had to append -es or -de to the end of the field to allow

Example of our old update process:
  • receive the zip file from Doc team (iOS_Ignition_ES_update2.zip) and unpack.
  •  edit the CSV file to include:
    •  datacategorygroup.products (lmiignitioniphone)
    • a unique Doc_Group__c (igniosuges)
    • and an edit to the URL name to prevent overlap/conflict with similar products (What-is-the-Host-ios-es2)
  •  then re-zip files and add the articleImportSampleContent.properties file
  •  In Salesforce, search for articles tagged with the same Doc_Group__c (igniosuges) and remove them from published docs due to URL name issue
  •  In Saleforce, under Data Management > Import Articles, choose the new zip file and import.
  •  After import, can then verify/review docs in draft and publish.
With the new knowledge multi-lingual support, we hoped it would solve this issue, but it does not, at least not for our needs.

When you follow their instructions for doing export, you'll find that they don't spell everything out, leaving you to assume some of the details, or having to run thought and figure it out by trial and error.

They also provide a 4min 'how-to' video, which walks you though the process of setting up the new multi-lingual support and the two ways they offer for translations, but it is more of an overview, and misses a few key issues with the import, mainly that the URL Name field needs to be Unique so it cant be the same as the original Article.

Then when your trying to do an export, besides being very cumbersome, the format of the export itself is not very usable, It would be nice to be able to export to .csv since you can import with it, but instead it gives you a zip of a bunch of folders with an individual document in each. If you try to export form the translation work bench, on the page where you choose what format you wish to export in, it says:

You can export three kinds of text
* Source - Produces one .csv file that contains all of the text that is translatable
* Untranslated - Produces a set of files, by language, with text that is not yet translated
* Bilingual - Produces a set of files by language that contain all of the text that is translatable

BUT the source does NOT produce a .csv file, it is instead a .stf file, which i am sure is useful for the correct program, but not what we need, or expected from the above instructions.

Overall i still think that knowledge's new multi-lingual features is a step in the right direction. It is definitely good if you are doing in-house editing, BUT I think that knowledge, in general, has a ways to go if they want orgs with more than 500 articles to utilize it efficiently.

Thursday, March 17, 2011

jQuery table sorter meets salesforce

jQuery is a powerful tool, and so I always use it in my APEX projects to help accomplish what I wouldn't be able to with just apex/html/css. One of the things I almost always include is the Tablesorter, but there are a few issues when trying to make it work with Salesfroce standard page markup tags. 
All the documentation for the tablesorter is on their page: Tablesorter, but some points to remember are that if you are using any apex:output(filed/text) that it wraps a span around the text and the tablesorter needs to know that, also if you use an 'a' tag it behaves differently as well. Also there seems to be an issue with using the default sorter params, ie letting the tablesorter pick them for you.
To show you a good example of the jQuery table sorter in action with salesforce, I will be using the 'User Info List' that I recently just updated with it.

(NOTE: that table sorter requires the class tablesorter  on the table tag in order to work)
Example Table markeup:
<td><apex:outputfield value="{!c.Email}" /></td>
<td>{!c.Name}</td>
<td><span><apex:outputfield value="{!c.Phone}" /></span></td>
<td><a target="_blank" href="/{!c.Id}" title="go to Contact">Contact</a></td>

JS markup:
//this checks the table for data on three levels
      //<td>the data</td> $(node).text();
      //<td><node>the data</node></td> $(node).next().text();
      //<td><node><node>the data</node></node></td> $(node).next().next().text();
//this ensures that the data is found no matter how many nodes it needs to transverse.
var myTextExtraction = function(node) 
{  
      var nodeText = $(node).text();
      if(nodeText == null || nodeText == ''){
            nodeText = $(node).next().text();
      }
      if(nodeText == null || nodeText == ''){
            nodeText = $(node).next().next().text();
      }
      return nodeText;
}
//example markup to use the node traversing and establish the sorter params
$("table").tablesorter({
       textExtraction: myTextExtraction,
       headers: {
           0: {sorter: 'text'},
           1: {sorter: 'text'},
           2: {sorter: 'text'},
           3: {sorter: 'text'},
           4: {sorter: 'shortDate'
       }
   });

Now lets look at the User-Info-List page, you'll notice that instead of using the standard apex:pageblocktable I am using a standard table, BUT I have included the style(classes/structure) of what the apex:pageblocktable would have... meaning that if you right click and view source on an apex:pageblocktable you will see the same as below (style/class wise minus a few things for my own customizations) and so when this page renders, it looks like an apex:pageblocktable. I do it this way due to the lack of flexibility in the standard apex:pageblocktable, and the blocktable seems to clash with the jQuery tablesorter when used naively. 


The Page:
<apex:page controller="UserInfoList" standardStylesheets="false" title="User Info" action="{!SwitchList}">
<apex:stylesheet value="{!URLFOR($Resource.UserList, '/NewTQC.css')}"/>
<apex:stylesheet value="{!URLFOR($Resource.UserList, '/style.css')}"/>
<script type="text/javascript" language="javascript" src="{!URLFOR($Resource.UserList, '/js/jquery1_4.js')}" ></script>
<!-- always include the tablesorter script after the jQuery -->
<script type="text/javascript" language="javascript" src="{!URLFOR($Resource.UserList, '/js/jquery.tablesorter.min.js')}" ></script>
<apex:form >
<div id="pageWrap">
<!--wrap the whole page in an outputpanel so that ajax rerenders, it triggers the jQuery.ready() function-->
<apex:outputPanel id="TicketListWrap" styleClass="Wrap">
      <script language="javascript" type="text/javascript">
            //controls the loading gif
            function Loading(b){
                  if(b){
                        $('.loadingIcon').removeClass('hideIt');
                  }else{
                        $('.loadingIcon').addClass('hideIt');
                  }
            }
            $(document).ready(function(){
                  //important to specify the sorting param otherwise it wont always choose the 'correct' one
                  $("table").tablesorter({
                    headers: {
                        0: {sorter: 'text'},
                        1: {sorter: 'text'},
                        2: {sorter: 'digit'},
                        3: {sorter: 'digit'},
                        4: {sorter: 'text'},
                        5: {sorter: 'text'},
                        6: {sorter: 'text'
                    }
                });
            });   
      </script>
      <div class="clear"></div>
      <br />
      <div id="newTicketHeader" class="floatL">Users in {!sDepartment}</div>
      <div class="floatR">
            <strong>Department:</strong>
            <apex:selectList id="department" size="1" value="{!sDepartment}" >  
                  <apex:selectOptions value="{!lstDepartmentOptions}" />
                  <!-- every time the select list is changed it refreshes the list of users -->
                  <apex:actionSupport event="onchange" onsubmit="Loading(true);" action="{!SwitchList}" rerender="TicketListWrap"id="actionsupportforKnownIssues" oncomplete="Loading(false);" />
            </apex:selectList>
      </div>
      <!-- the loading icon -->
      <div class="loadingIcon hideIt"><apex:image id="loadingImage" value="{!URLFOR($Resource.UserList, 'images/loader_24x26.gif')}"width="24" height="26"/></div>
      <div class="clear"></div>
      <div style="margin-left:5px; font-size:10px;">User count: {!iUserCount}</div>
      <table id="Usertable" class="list tablesorter" cellspacing="1">
      <thead class="rich-table-thead">
            <tr class="headerRow">
                  <th colspan="1" scope="col">Name</th>
                  <th colspan="1" scope="col">Phone</th>
                  <th colspan="1" scope="col" style="width:50px;">Ext</th>
                  <th colspan="1" scope="col">ICQ</th>
                  <th colspan="1" scope="col">Title</th>
                  <th colspan="1" scope="col">Location</th>
                  <th colspan="1" scope="col">Action</th>
            </tr>
      </thead>
      <tbody>
            <apex:repeat value="{!CurrentListofUsers}" var="u" id="UserListRepeater">
            <tr class="dataRow">
                  <td>{!u.name}</td>
                  <td>{!u.phone}</td>
                  <td>{!u.Extension}</td>
                  <td>{!u.ICQ__c}</td>
                  <td>{!u.Title}</td>
                  <td>{!u.Location__c}</td>
                  <td>
                        <div>
                              <!-- only current user can edit their own info -->
                              <apex:outputlink value="/{!u.id}/e?retURL=apex/UserInfoList" rendered="{!IF(u.id == $User.Id, true, false)}" >Edit Info</apex:outputlink>
                        </div>
                              <!-- only manager's/Team lead's/VP's can edit the users Queue info -->
                        <apex:outputpanel rendered="{!IF(CONTAINS($UserRole.Name, 'Manager') || CONTAINS($UserRole.Name, 'VP') || CONTAINS($UserRole.Name, 'Team Lead'), true, false)}">
                              <apex:outputlink value="/apex/QueueMemberEdit?UserId={!u.id}" >Edit Queues</apex:outputlink>
                        </apex:outputpanel>
                        
                  </td>
            </tr>
            </apex:repeat>
      </tbody></table>
      <hr />
</apex:outputPanel>
</div>
</apex:form>
</apex:page>



The classes that power it:
public with sharing class UserInfoList {

      public string sDepartment{get;set;}
      public string sCurrentUserDepartment
      {
            get{return UserInfoListHelper.FindDepartment(UserInfo.getUserId());}
            set;
      }
      public List<User> CurrentListofUsers
      {
            get{return UserInfoListHelper.UserList( UserInfoListHelper.FindDepartment(sCurrentUserDepartment, sDepartment));}
            set;
      }
      public integer iUserCount
      {
            get{return CurrentListofUsers.size();}
            set;
      }
      public List<selectOption> lstDepartmentOptions
      {
            get{return UserInfoListHelper.DepartmentKeys();}
            set{lstDepartmentOptions = value;}
      }
            
      public pageReference SwitchList()
      {
            if(!GlobalHelper.CheckForNotNull(sDepartment))
            {
                  sDepartment = sCurrentUserDepartment;
            }
            return null;
      }
}

public with sharing class UserInfoListHelper {
      /// <summary>
      /// OVERLOADED
    /// FINDS THE USERS DEPARTMENT BASED ON ID PASSED
    /// </summary>
    /// <param name="lstInValueToCheck">USER ID</param>
    /// <returns>USER DEPARTMENT</returns>
      public static string FindDepartment(string sInUserId)
      {
            if(GlobalHelper.CheckForNotNull(sInUserId))
            {
                  try
                  {
                        User u = [select id, Department from User where id=:sInUserId limit 1];
                        return u.Department;
                  }
                  catch(exception e)
                  {
                        system.debug('UserInfoListHelper - FindDepartment - error: '+e);
                  }     
            }
            return null;
      }
      /// <summary>
      /// OVERLOADED
    /// RETURNS USER DEPARTMENT IF SELECTED DEPARMENT IS NULL
    /// </summary>
    /// <param name="sInDepartmentSelection">USER DEPARMENT</param>
    /// <param name="sInUserDepartment">SELECTED DEPARTMENT</param>
    /// <returns>DEPARTMENT</returns>
      public static string FindDepartment(string sInUserDepartment, string sInDepartmentSelection)
      {
            if(GlobalHelper.CheckForNotNull(sInDepartmentSelection))
            {
                  return sInDepartmentSelection;
            }
            return sInUserDepartment;
      }
      /// <summary>
    /// ADDS DEPARTMENT KEY NAMES TO LIST OF SELECT OPTIONS
    /// </summary>
    /// <param name="lstInUser">USER ID</param>
    /// <returns>DEPARTMENT SELECT OPTION LIST</returns>
      public static List<selectOption> AddItemToList(List<User> lstInUser)
      {
            List<SelectOption> lstOptions = new List<SelectOption>();
            if(GlobalHelper.CheckForNotNull(lstInUser))
            {
                  for(User u : lstInUser)
                  {
                        if(!GlobalHelper.ContainsItem(u.Department, lstOptions))
                        {
                              lstOptions.add(new SelectOption(u.Department,u.Department, false));
                        }
                  }
                  return lstOptions;
            }
            return null;
      }
      /// <summary>
    /// FINDS ALL DEPARTMENTS AND ADDS THEM TO LIST OF SELECT OPTIONS
    /// </summary>
    /// <returns>DEPARTMENT SELECT OPTION LIST</returns>
      public static List<selectOption> DepartmentKeys()
      {
            User[] u = [Select Department From User where Department != null and IsActive = True limit 200];
            if(GlobalHelper.CheckForNotNull(u))
            {
                  return UserInfoListHelper.AddItemToList(u);
            }
            return null;
      }
      /// <summary>
    /// GETS LIST OF USERS BASED ON THE DEPARTMENT PASSED
    /// </summary>
    /// <param name="sInDepartment">DEPARTMENT</param>
    /// <returns>USER LIST</returns>
      public static List<User> UserList(string sInDepartment)
      {
            if(GlobalHelper.CheckForNotNull(sInDepartment))
            {
                  try
                  {
                        User[] u = [select Name, Phone, Extension, Title, ICQ__c, Location__c,Department from User where IsActive = True AND Department =:sInDepartment Order By Name ASC limit 200];
                        return u;
                  }
                  catch(exception e)
                  {
                        system.debug('UserInfoListHelper - UserList - error: '+e);
                  }
            }
            return null;
      }
}
Questions? 
Twitter: @SalesForceGirl  or Facebook