Showing posts with label queue. Show all posts
Showing posts with label queue. 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.

Friday, March 18, 2011

Queue-Member Edit Customizations

Using the standard Salesforce Queue edit pages for users can be somewhat tiresome, especially if you need to make mass changes. Not to mention that the amount of page refreshes it requires to edit more than 5 users, takes enough time to make you want to throw your keyboard. So to get around that, making a custom page to stream line the process.


 

In my Org I created a User-List page which shows each user by department, in a sortable list/table(more about this & how to make it here) that allows for easy access to a users contact info. From this table, Manager's/TeamLead's/VP's, can click 'edit' next to a user and get taken to a QueueMember edit page witch shows all Queues and whether or not the user belongs to it. Then I also made it so the Edit part of the page was as easy as a click and the page wouldn't need to be refreshed at all.

What this would entail is a userId passed to the page so we know what User we are talking about. Then it would need to get a list of all Queues and then ones only specific for that user. If they wanted to add the user to the Queue, the Queue Id would need to be passed back to the controller, and the same for removing the user from the Queue. Then we update the page with the new User specific Queues and repeat.

public with sharing class QueueMemberEditCon {

      public string sUserName{get;set;}
      public string sQueueId{get;set;}
      public string sUserId{get;set;}
      
      public List<GroupMember> lstFullGroupMemberList{get{returnQueueMemberHelper.FindGroupIdsFromMembers(sUserId);}set;}
      public List<QueueSobject> lstFullQueueList{get{returnQueueMemberHelper.FindQueueList();}set;}
      
      public Pagereference loadUser()
      {
            sUserId = GlobalHelper.UrlParam('UserId');
            if(GlobalHelper.CheckForNotNull(sUserId))
            {
                  sUserName = QueueMemberHelper.UserName(sUserId);
            }
            return null;
      }
      public pageReference RemoveUserFromQueue()
      {
            sQueueId = GlobalHelper.UrlParam('QueueId');
            sUserId = GlobalHelper.UrlParam('UserId');
            if(GlobalHelper.CheckForNotNull(sQueueId) && GlobalHelper.CheckForNotNull(sUserId))
            {
                  QueueMemberHelper.RemoveUserFromQueue(sQueueId, sUserId);
            }
            return null;
      }
      
      public pageReference AddUserToQueue()
      {
            sQueueId = GlobalHelper.UrlParam('QueueId');
            sUserId = GlobalHelper.UrlParam('UserId');
            if(GlobalHelper.CheckForNotNull(sQueueId) && GlobalHelper.CheckForNotNull(sUserId))
            {
                  QueueMemberHelper.AddUserToQueue(sQueueId, sUserId);
            }
            return null;
      }
}

All the work is done in my helper class, but it still only has a few methods in it. The first one retrieves the entire Queue list for the sObject Case, where as the next one Finds the one that are specific to the UserId that is passed. This could technically be split into two methods, but that's up to you. Then of course there is the RemoveUserFromQueue/AddUserToQueue methods which are void but could be booleans so we can base the next steps on whether or not the method was successful, but I decided against it for now. And finally the UserName method which should explain itself. 

public with sharing class QueueMemberHelper {
      /// <summary>
    /// FINDS LIST OF QUEUES UNDER CASE
    /// </summary>
    /// <returns>LIST OF QUEUES</returns>
      public static List<QueueSobject> FindQueueList()
      {
            QueueSobject[] q = [Select Id, QueueId, Queue.Name From QueueSobject whereSobjectType = 'Case' limit 100];
            return q;
      }
      /// <summary>
    /// FINDS USER IN ALL GROUP MEMEBER ITEMS
    /// </summary>
    /// <param name="sInUserId">USER ID</param>
    /// <returns>LIST OF QUEUES</returns>
      public static List<GroupMember> FindGroupIdsFromMembers(string sInUserId)
      {
            if(GlobalHelper.CheckForNotNull(sInUserId))
            {
                  List<string> sList = new List<string>();
                  List<GroupMember> gList = new List<GroupMember>();                  
                  try
                  {
                        GroupMember[] gm = [Select Id, GroupId, Group.Name, UserOrGroupId FromGroupMember Where UserOrGroupId =:sInUserId limit 100];
                        if(GlobalHelper.CheckForNotNull(gm))
                        {
                              for(GroupMember g : gm)
                              {
                                    if(!GlobalHelper.ContainsItem(g.Group.Name, sList))   
                                    {
                                          gList.add(g);
                                    }
                              }
                              return GlobalHelper.AddItemsToList(gList);
                        }
                  }
                  catch(exception e)
                  {
                        system.debug('Oops... QueueMemberHelper - FindGroupIdsFromMember - Error: ' + e);
                        return null;
                  }
            }
            return null;
      }
      /// <summary>
    /// FINDS USER NAME
    /// </summary>
    /// <param name="sInUserId">USER ID</param>
    /// <returns>USER NAME</returns>
      public static string UserName(string sInUserId)
      {
            try
            {
                  User u = [select Name from User where id=:sInUserId limit 1];
                  return u.Name;
            }
            catch(exception e)
            {
                  system.debug('UserInfoListHelper - UserName - error: '+e);
            }     
            return null;
      }
      /// <summary>
    /// REMOVED USER FROM QUEUE MEMBER
    /// </summary>
    /// <param name="sInQueueId">QUEUE ID</param>
    /// <param name="sInUserId">USER ID</param>
      public static void RemoveUserFromQueue(string sInQueueId, string sInUserId)
      {
            try
            {
                  GroupMember gm = [Select Id, GroupId, UserOrGroupId From GroupMember whereUserOrGroupId =:sInUserId and GroupId =:sInQueueId limit 1];
                  if(gm != null)
                  {
                        try
                        {
                              delete gm;
                        }
                        catch(exception e)
                        {
                              system.debug('Oops... QueueMemberHelper - RemoveUserFromQueue - Error: ' + e);
                        }
                  }
            }
            catch(exception e)
            {
                  system.debug('Oops... QueueMemberHelper - RemoveUserFromQueue - Error: ' + e);
            }
            
      }
      /// <summary>
    /// ADDS USER TO QUEUE MEMBER
    /// </summary>
    /// <param name="sInQueueId">QUEUE ID</param>
    /// <param name="sInUserId">USER ID</param>
      public static void AddUserToQueue(string sInQueueId, string sInUserId)
      {
            GroupMember gm = new GroupMember(
                  GroupId = sInQueueId,
                  UserOrGroupId = sInUserId
            );
            try
            {
                  insert gm;
            }
            catch(exception e)
            {
                  system.debug('Oops... QueueMemberHelper - AddUserToQueue - Error: ' + e);
            }
      }
}

So on the Page I used the jQuery table sorter(more info here), and in the apex:repeat parts, you will see I do some 'ninja' to make sure that it displays the correct info. In the apex:repeat I use rendered="{!IF(q.Queue.Name == u.Group.Name, true, false)}" to ensure that it only shows that they have that Queue if the two lists match. Then I use jQuery to Hide or Show the 'Add' or 'Remove' links based on if that field has content.

<apex:page controller="QueueMemberEditCon" standardStylesheets="false" title="Queue Member Edit"action="{!loadUser}">
<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>
<script type="text/javascript" language="javascript" src="{!URLFOR($Resource.UserList, '/js/jquery.tablesorter.min.js')}" ></script>
<apex:form >
<div id="pageWrap" style="width:85%; margin:auto;">
<apex:outputPanel id="TicketListWrap" styleClass="Wrap">
      <script language="javascript" type="text/javascript">
            function Loading(b)
            {
                  if(b){
                        $('.loadingIcon').removeClass('hideIt');
                  }else{
                        $('.loadingIcon').addClass('hideIt');
                  }
            }
            function handleMembers()
            {
                  $('.memberCol').each(function(){
                  if($(this).find('.IsMember').text().trim() == 'Remove')
                  {
                        $(this).find('.IsMember').removeClass('hideMe');
                  }else{
                        $(this).find('.NotMemeber').removeClass('hideMe');
                  }
                });
            }
            $(document).ready(function(){
                  $("table").tablesorter({
                    headers: {
                        0: {sorter: 'text'},
                        1: {sorter: 'text'},
                        2: {sorter: 'text'},
                        3: {sorter: 'text'},
                        4: {sorter: 'text'},
                        5: {sorter: 'text'},
                        6: {sorter: 'text'
                    }
                });
                handleMembers();
            });   
      </script>
      <apex:outputpanel id="queueList">
            <div class="clear"></div>
            <div id="newTicketHeader" class="floatL">
                  Showing Queue's for&nbsp;{!sUserName}
            </div>
            <div class="floatR">
                  <apex:outputlink value="/apex/UserInfoList" onClick="Loading(true);">Back to Users</apex:outputlink>
            </div>
            <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>
            <table id="Usertable" class="list tablesorter" cellspacing="1" style="width:85%; margin:auto;">
            <thead class="rich-table-thead">
                  <tr class="headerRow">
                        <th colspan="1" scope="col">Queues</th>
                        <th colspan="1" scope="col">Is Member</th>
                        <th colspan="1" scope="col">Action</th>
                  </tr>
            </thead>
            <tbody>
                  <apex:repeat value="{!lstFullQueueList}" var="q" id="QueueListRepeater">
                  <tr class="dataRow">
                        <td>{!q.Queue.Name}</td>
                        <td>
                              <apex:repeat value="{!lstFullGroupMemberList}" var="u">
                                    <apex:outputpanel rendered="{!IF(q.Queue.Name == u.Group.Name, true, false)}">
                                          Yes
                                    </apex:outputpanel>
                              </apex:repeat>
                        </td>
                        <td class="memberCol">
                              <div class="hideMe IsMember">
                              <apex:repeat value="{!lstFullGroupMemberList}" var="u">
                                    <apex:outputpanel rendered="{!IF(q.Queue.Name == u.Group.Name, true, false)}">
                                          <apex:CommandLink value="Remove"onClick="Loading(true);" action="{!RemoveUserFromQueue}" rerender="TicketListWrap"oncomplete="Loading(false);">
                                                <apex:param name="QueueId" value="{!q.QueueId}"/>
                                          </apex:CommandLink>
                                    </apex:outputpanel>
                              </apex:repeat>
                              </div>
                              <div class="hideMe NotMemeber">
                                    <apex:CommandLink value="Add" onClick="Loading(true);"action="{!AddUserToQueue}" rerender="TicketListWrap" oncomplete="Loading(false);">
                                          <apex:param name="QueueId" value="{!q.QueueId}"/>
                                    </apex:CommandLink>
                              </div>
                        </td>
                  </tr>
                  </apex:repeat>
            </tbody></table>
      </apex:outputpanel>
      </apex:outputpanel>
</div>
</apex:form>
</apex:page>

Questions? 
Twitter: @SalesForceGirl  or Facebook