Showing posts with label visualforce pages. Show all posts
Showing posts with label visualforce pages. Show all posts

Tuesday, June 4, 2013

Showing images in your custom Visualforce pages via Attachments

Everyone wants images, profile pictures, logos etc. Sure you can add these via style, but this isn't very practical if the end user is adding them (they dont know, or need to know, how to use static resources). So lets say you have a task to make a page that shows lists of contacts, but they want to see the contacts image, if they have one. If they dont, we will have to show a default user image in its place to keep each contact consistant. 

This is done very simply :-) On each contact record (or any object that you allow attachments on) the user has the ability to attach files and images. So we want to grab them via a relationship query on that object, and then display them on the page. For my example I am only going to show the most recent image per Contact.

First step is to create the page and controller. I called my class (controller) sfg_ContactPhotos so the page so far would look like this:


<apex:page showHeader="true" sidebar="true" controller="sfg_ContactPhotos">

</apex:page>

In the controller we will need a List of Contacts and a few methods to get it all going. Normally this would be a controller extension to the Contact standard controller, but since I just want to show the logic, I dont want to complicate things here. 



public class sfg_ContactPhotos {

  public List<Contact> lstOfContacts {get; set;}


  public sfg_ContactPhotos() {

    //constructor
  }

  private List<Contact> findContacts() {

    //logic for getting contacts
    return new List<Contact>();
  }

}



You may notice that I like to use the {get; set;} instead of the way salesforce normally does it via a get method and set method; this is simply because if it is done the salesforce way, the get method will be hit on every call to the controller and I only need to load the Contacts once. Anyhow, we need to add another variable for the Map of Attachments, a method to build the map, and it wouldn't hurt to wire up the variables to their methods in the constructor. If you notice the Map is a <String, String> which is not the *correct way, it should be Map<Id, Attachment> but due to issue with visualforce and Maps, it has to be String (see Post).


public class sfg_ContactPhotos {

  public List<Contact> lstOfContacts {get; set;}

  public Map<String, String> mapOfPhotos {get; set;}

  public sfg_ContactPhotos() {

    lstOfContacts = findContacts();
    mapOfPhotos = buildPhotoMap(lstOfContacts);
  }

  private List<Contact> findContacts() {

    //logic for getting contacts
    return new List<Contact>();
  }

  private Map<String, String> buildPhotoMap(List<Contact> pContacts) {

    if (pContacts  == null || pContacts.isEmpty()) {
      return new Map<String, String>();
    }

    Map<String, String> returnMap = new Map<String, String>();

    //logic to make the map of attachments
    return returnMap;
  }

}



A few things to note here, if you notice I am passing the list of Contacts into the buildPhotoMap method. I call it 'pContacts' due to it being a 'param' thus the 'p'. Since it is passed, this means that we can control what builds the map, instead of just grabbing the Contacts in their current state. I also check to see if what is passed actually has anything before creating any variables in the class, this is a good practice to get into. Why waste time creating variables if there isn't anything for the method to use? It may only be milliseconds but it can add up, so its good to do the check for null/empty/blank first and if nothing is there, exit the method, not wasting any time. 


Ok, lets add some logic to the class, first we will query for the Contacts and the related Attachments, then pass it to the buildPhotoMap method to get the data set up like we want. In this query we only want the public ones and will order the Attachments by LastModifiedDate 'desc' which pust the newest one first. We will limit the query to 2 just so we dont over load the page/example.


private List<Contact> findContacts() {
    return [select Name,
                   Email,
                   (select ContentType
                    from Attachments
                    where isPrivate = false
                    order by LastModifiedDate desc)
            from Contact
            limit 2];
  }


If you notice I am not querying for any Id's this is because by default SOQL returns the Id of the object so there is no need to specify it in the query. There is also no need for a try catch statement since if there is nothing to return it will simply return null, and in the buildPhotoMap method we are if it is null or empty before using it, thus negating any issues.

Now lets put some logic into the buildPhotoMap method, we will need to iterate over the contact list with a for loop and since the Attachments are in a related query we will need a second for loop over it to get into each attachment on the individual contact record. Once in the Attachments for loop we will need to check what type it is since you can have many different types of attachments, we should check if the ContentType is of type 'image'. (you could of corse add a where clause to the query itself and only return Attachments where ContentType = 'image/%' thus negating the check in the for loop)



private Map<String, String> buildPhotoMap(List<Contact> pContacts) {
    if (pContacts.isEmpty()) {
      return new Map<String, String>();
    }

    Map<String, String> returnMap = new Map<String, String>();
    for (Contact c : pContacts) {
      for (Attachment a : c.Attachments) {
        if (returnMap.containsKey(c.Id)) {
          break;
        }
        if (a.ContentType.contains('image/')) {
          returnMap.put(c.Id, a.Id);
          break;
        }
      }
      if (!returnMap.containsKey(c.Id)) {
        returnMap.put(c.Id, 'none');
      }
    }
    return returnMap;
  }


Since we need a map entry for each contact (so we dont brake the page with an error of 'Map key {ID} not found in map') and we dont want to loop more than we need to, I set it up so it brakes once it detects that the map already contains the contact Id. If it doesn't then it will check if the Attachment is of type 'image/' (normally the types are 'image/jpg' or 'image/gif' etc so using 'image/' will cover all images) and if it is, add it to the map and then brake out of the Attachment for loop and on to the next contact. If however, the Contact doest have any image Attachments, we will add a null value so the map wont brake the page.

Now that we have the controller done, lets focus on the page. We will need an apex:repeat over the contact list and an apex:image to display the default image (incase the user has no images)


<apex:page showHeader="true" sidebar="true" controller="sfg_ContactPhotos">
    <apex:stylesheet value="{!URLFOR($Resource. sfg_customPhotos, '/css/main.css')}" />
 
    <div id="sfg_pageWrap">
        <apex:repeat value="{!lstOfContacts}" var="c">

            <div id="{!c.Id}">
                <h1>{!c.Name}</h1>
                <apex:image url="{!URLFOR($Resource.sfg_customPhotos, '/image/userIcon.png')}" width="50" height="50"/>
            </div>

        </apex:repeat>
    </div>
</apex:page>


As you can see I have some style included via the apex:stylesheet tag, and a default image. We still need to add some conditional rendering and the Attachment Image to the page but first lets take a moment to talk about Resources.

Static Resources are very hand if you know how to use them. First off they are located in setup > develop > Static Resources. Click the new button and you'll see a page like this:

 The name will be the one used in the value="{!URLFOR($Resource.yourResourceNameHere,  ... ", the resource should be public, and the file should be a zip of the folder (if you want to have everything in one spot, otherwise you can just upload an image or a file, but if you want folders they have to be compressed or 'zipped'). Since I am including everything in one resource I have to zip my folders, which look like this on my computer:

So the page should now look like this once we fill it all out:
Click save and then if you need to update it you can click edit and upload the new .zip.

Since I am using folders, I have to travers them like would in any other language to access the items like '/css/main.css' as you can see in the page. Its the same for the image as well, '/image/userIcon.png'. Even if you dont have folders but a zip of lose files, you will need to '/someFileName.css' the '/' is important, without it some browsers (like IE) wont render it correctly.

Ok back to business, lets add the Attachment image to the page and then put in some conditional rendering based off weither the map is empty for that contact, and if so lets show a default image of a user:

<apex:image rendered="{!mapOfPhotos[c.Id] == 'none'}" url="{!URLFOR($Resource.sfg_customPhotos, '/image/userIcon.png')}" width="50" height="50"/>

<apex:image rendered="{!mapOfPhotos[c.Id] != 'none'}" url="{!URLFOR($Action.Attachment.Download, mapOfPhotos[c.Id])}" width="50" height="50" />

SO when we load the page we should see something like this:
...and yes .gif's do work :-)

Monday, May 27, 2013

Bug with apex:selectCheckboxes

The Project:
Make a search with multi-select checkboxes for better filtering. 

The Solution:
Simple right? Just make a page and controller with some <apex:selectCheckboxes> and a <apex:commandButton> to make it all go. 

<apex:page showHeader="true" sidebar="true" controller="sfg_testBugWithActionButton">
<apex:form>
    <apex:outputpanel id="mainWrap">
        <apex:repeat value="{!filterMap}" var="key">
          <div class="filterItem">
            <h2>{!key}</h2>
            <apex:selectCheckboxes value="{!filterKeys[key]}" layout="pageDirection">
              <apex:selectOptions value="{!filterMap[key]}" />
            </apex:selectCheckboxes>
          </div>
        </apex:repeat>
        <apex:commandButton action="{!preformAction}" rerender="renderWrap" value="Submit Action" />
      <apex:outputpanel id="renderWrap">
        {!resultString}
      </apex:outputpanel>
    </apex:outputpanel>
  </apex:form>
</apex:page>



public class sfg_testBugWithActionButton {

  public List<String> fGender {get; set;}

  public List<String> fGrade {get; set;}

  public List<String> fRole {get; set;}

  public String resultString {get; set;}



  public Map<String, List<SelectOption>> filterMap {get; set;}
  public Map<String, String> filterKeys {get; set;}

  public sfg_testBugWithActionButton() {
    filterKeys = new Map<String, String> {
      'Gender' => 'fGender',
      'Grade' => 'fGrade',
      'Role' => 'fRole'
    };
    createfilterMap();
    resultString = 'on Load of page';
  }

  public PageReference preformAction() {
    resultString = 'button action preformed';
    return null;
  }

  private void createfilterMap() {
    filterMap = new Map<String, List<SelectOption>>();
    List<SelectOption> options = new List<SelectOption>();

    for (String s : filterKeys.keySet()) {
      if (s == 'Gender') {
        options = new List<SelectOption>();
        options.add(new SelectOption('Male', 'Male'));
        options.add(new SelectOption('Female', 'Female'));
        filterMap.put('Gender', options);
      }
      if (s == 'Grade') {
        options = new List<SelectOption>();
        options.add(new SelectOption('A', 'A'));
        options.add(new SelectOption('B', 'B'));
        options.add(new SelectOption('C', 'C'));
        filterMap.put('Grade', options);
      }
      if (s == 'Role') {
        options = new List<SelectOption>();
        options.add(new SelectOption('Support', 'Support'));
        options.add(new SelectOption('Sales', 'Sales'));
        options.add(new SelectOption('Marketing', 'Marketing'));
        filterMap.put('Role', options);
      } 
    }
  }
}

(no style so nothing pretty, just to get the idea)

But WAIT! Why isn't anything happening when I click the button? The text next to the button should be changing. hummm Lets put a debug at the top of the method being called by the button and check the logs to see if its hit...

public PageReference preformAction() {
  system.debug('Gender: ' + fGender);
  system.debug('Grade: ' + fGrade);
  system.debug('Role: ' + fRole);
  resultString = 'button action preformed';
  return null;
}

hummm nothing.. not even a mention of the button getting clicked in the logs, let alone my debug statements. Ok I know from experience I can force the button by using the <apex:actionRegion> attribute..

<apex:actionRegion>
  <apex:commandButton action="{!preformAction}" rerender="renderWrap" value="Submit Action" />
</apex:actionRegion>
      
Click and well now the debug statement is being hit, but its not picking up the values in the  <apex:selectCheckboxes> hummmm.. I know I shouldnt need the <apex:actionRegion> so lets remove that and see what happens when i just click the button, without selecting any of the checkboxes first.. Oh shit it works... the values are null, but they should be since nothing is selected.. Ok lets try changing the <apex:selectCheckboxes> to <apex:selectList> and see if they work.. damn, it seems to work, the resultsString is getting updated, but the values are still showing as null in the logs so I guess its not working, but atleast its working better than the <apex:selectCheckboxes>.. 

It shouldn't be anything to do with the maps or repeat but why dont we do this the long way and see if that helps..

<apex:page showHeader="true" sidebar="true" controller="sfg_testBugWithActionButton">
<apex:form>
    <apex:outputpanel id="mainWrap">
      <div class="filterItem">
        <h2>Grade</h2>
        <apex:selectCheckboxes value="{!fGrade}" layout="pageDirection">
          <apex:selectOptions value="{!soGrade}" />
        </apex:selectCheckboxes>
      </div>
      <div class="filterItem">
        <h2>Gender</h2>
        <apex:selectCheckboxes value="{!fGender}" layout="pageDirection">
          <apex:selectOptions value="{!soGender}" />
        </apex:selectCheckboxes>
      </div>
      <div class="filterItem">
        <h2>Role</h2>
        <apex:selectCheckboxes value="{!fRole}" layout="pageDirection">
          <apex:selectOptions value="{!soRole}" />
        </apex:selectCheckboxes>
      </div>
      <apex:commandButton action="{!preformAction}" rerender="renderWrap" value="Submit Action" />
      <apex:outputpanel id="renderWrap">
        {!resultString}
      </apex:outputpanel>
    </apex:outputpanel>
  </apex:form>
</apex:page>


public class sfg_testBugWithActionButton {
  public String fGender {get; set;}
  public String fGrade {get; set;}
  public String fRole {get; set;}
  public List<SelectOption> soGender {get; set;}
  public List<SelectOption> soGrade {get; set;}
  public List<SelectOption> soRole {get; set;}
  public String resultString {get; set;}

  public sfg_testBugWithActionButton() {
    createfilterMap();
    resultString = 'on Load of page';
  }

  public PageReference preformAction() {
    system.debug('Gender: ' + fGender);
    system.debug('Grade: ' + fGrade);
    system.debug('Role: ' + fRole);
    resultString = 'button action preformed';
    return null;
  }

  private void createfilterMap() {
    soGender = new List<SelectOption>();
    soGender.add(new SelectOption('Male', 'Male'));
    soGender.add(new SelectOption('Female', 'Female'));
      
    soGrade = new List<SelectOption>();
    soGrade.add(new SelectOption('A', 'A'));
    soGrade.add(new SelectOption('B', 'B'));
    soGrade.add(new SelectOption('C', 'C'));
    
    soRole = new List<SelectOption>();
    soRole.add(new SelectOption('Support', 'Support'));
    soRole.add(new SelectOption('Sales', 'Sales'));
    soRole.add(new SelectOption('Marketing', 'Marketing'));
  }
}

No joy, same problem with or without the repeat when using <apex:selectCheckboxes>. Now lets try the same thing but with <apex:selectList> instead.. And it works! But why wouldn't it work when in the repeat or with the <apex:selectCheckboxes>? I also made sure it works when I added the attribute multiselect="true" to them, since that is what I would need anyhow if I had to use Lists over checkboxes.

So lets review: 
Right now it seems that anything with the <apex:selectCheckboxes> wont work (regardless of version since I did try bumping it back on both the page and controller). When trying to be dynamic and using a repeat to populate the Lists it fails as well for both <apex:selectCheckboxes> and <apex:selectList>. But if you do it the long way, you can get <apex:selectList> to work at the very least, although that means more fighting with style to get it to look like checkboxes if that is the desired effect. 

*sad panda*

I posted a simpler test of this not working here: Stackoverflow

**Update**
ANSWER FOUND and so not a bug lol bellow is the update made to the controller, and no update was needed to the page(see page at top of post for code):


public class sfg_testBugWithActionButton {

  public List<String> fGender {get; set;}
  public List<String> fGrade {get; set;}
  public List<String> fRole {get; set;}

  public String resultString {get; set;}

  public Map<String, List<SelectOption>> filterMap {get; set;}

  //this had to changed from Map<String, String> to what you see below
  public Map<String, List<String>> filterKeys {get; set;}

  public sfg_testBugWithActionButton() {
    //this was also added, and helped make it all work
    fGender = new List<String>();
    fGrade = new List<String>();
    fRole = new List<String>();

    // and so this got updated as well to use the param and not a string
    filterKeys = new Map<String, List<String>> {
      'Gender' => fGender,
      'Grade' => fGrade,
      'Role' => fRole
    };
    createfilterMap();
    resultString = 'on Load of page';
  }

  public PageReference preformAction() {
    resultString = 'button action preformed';
    return null;
  }

  private void createfilterMap() {
    filterMap = new Map<String, List<SelectOption>>();
    List<SelectOption> options = new List<SelectOption>();

    for (String s : filterKeys.keySet()) {
      if (s == 'Gender') {
        options = new List<SelectOption>();
        options.add(new SelectOption('Male', 'Male'));
        options.add(new SelectOption('Female', 'Female'));
        filterMap.put('Gender', options);
      }
      if (s == 'Grade') {
        options = new List<SelectOption>();
        options.add(new SelectOption('A', 'A'));
        options.add(new SelectOption('B', 'B'));
        options.add(new SelectOption('C', 'C'));
        filterMap.put('Grade', options);
      }
      if (s == 'Role') {
        options = new List<SelectOption>();
        options.add(new SelectOption('Support', 'Support'));
        options.add(new SelectOption('Sales', 'Sales'));
        options.add(new SelectOption('Marketing', 'Marketing'));
        filterMap.put('Role', options);
      } 
    }
  }
}


HAZA! :-)

Wednesday, December 28, 2011

easy double-click to edit related-list VF page

Often enough I am asked to make a custom VisualForce of a related list that is editable in a similar way to salesforce's native double click to edit. I have made them in a variety of ways, including adding images like salesforce's to indicate if a field is editable or locked etc. and some simpler ways like the one I am going to do now.

The Problem:
a sObject under Account(or any other obj) needs show on the Account page layout, but it also needs to be double-click to edit to make it easy for the reps to update key information on the fly without having to click into each sObject. (for this example we will use the Contact obj)


Setup:
First we need to create a controller extension so that it can sit in the page layout of the Account obj, and even though we are showing the contact obj, the extension needs to point to account otherwise it wont show up as an available VisualForce page to add on the layout.
ContactInLineEdit.cls
ContactInLineEdit.page

This is a standard controller extension setup, this allows the class to use the account id referenced in the URL when the page is accessed. Example page URL will be 'apex/ContactInLineEdit?id={!account.id}'

Now we should add the Contact list to the class so we can access it on the page in a apex:repeat, when we add it to the class, we only need to get the list on load of the page, and then again when we update the fields.
ContactInLineEdit.cls

On the page we will need to add the apex:form tag, along with an apex:outputpanel with an id so we can call it later in a rerender attribute on the update call. We will also add the CSS and jQuery files that will help us control the double-click to edit functionality. We then place the jQuery inside the outputpanel so when it rerenders it can reapply the jQuery to the page. I also prefer to use the jQuery.noConflict(); since I often run into issues with salesforce's JS conflicting with what I am trying to implement.
ContactInLineEdit.page

Now we need to add the apex:pagemessages so we can do error handling and we need to add the table for the apex:repeat to sit in. We will also add the apex:outputfiled's that will display in the related list on the page, we need to make sure that any field we reference here is also in the select query on the class.
ContactInLineEdit.page

Lets add in the loading icon for the update and a JS function to control it, and we should also add the edit fields to the table. To help control the conditional hiding and showing of the fields for the edit, we should add some div's with classes to reference in jQuery.
ContactInLineEdit.page
ContactInLineEdit.page

Lets add the double click functionality, first we need to add a class to the edit text in the action column  <span class="editLink">Edit</span> then we need to add a function so that everytime the nonEdit field (outputField) is double-clicked it will hide the outputField and show the inputField. We will also change the Edit text to Cancel and add a class to it so we know that row is in edit mode.

Now lets add the edit row/cancel row edits functionality, this will allow all editable fields in the row to be edited and if cancel is hit, it will undo all edits by retrieving the text from the hidden outputfield otherwise it will continue to show the updated text since we are just hiding and showing. We don't want to clear the fields cause that will potentially clear out the saved info if it gets saved by mistake. So by retrieving the info from the outputfield, we are able to set it back to what it was originally.
ContactInLineEdit.page

We will also need to update our edit action to use the new function, it will now look like this: <span class="editLink" onclick="EditMe(jQuery(this));">Edit</span>
At this point we need to make the update method and add it to the page, this will allow any edits made by the user to be saved, and then the list will rerender with the new information.
ContactInLineEdit.cls

ContactInLineEdit.page

And that it, simple. I didn't add it here, but field validation could be added to ensure its a proper email or phone number etc. In the style i made so that fields that were editable when the mouse hovered over it, the cursor would change to a pointer like it would when the mouse hovers over a link. This way the user knows the field is editable instead of having to add an image that could bog down the page load.

Questions?  
Twitter: @SalesForceGirl  or Facebook

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