Service Portal User Directory, Part II

“Twenty years from now you will be more disappointed by the things that you didn’t do than by the ones you did do.”
Mark Twain

I did not really intend for this to be a multi-part exercise, but I ran into a little problem last time and so I needed a little time to come up with a solution. The problem, you may recall, is that I changed the destination page on the table widget options to the user_profile page, and now clicking on a department or location brings you to that page, where it cannot find a user with that sys_id. We definitely have a way around that by using the built-in reference page map to map a different page to references from those tables, but the question is, where do we want to send regular users who click on those links? I know I do not want to send them to the form page, so I went looking for some other existing page focused on departments or locations. Not finding anything to my liking, I resigned myself to the fact that I was going to have to come up with something on my own, and started thinking about what it is that I would like to see.

Since this is User Directory, I decided that what would really be interesting, and potentially useful, would be a list of Users assigned to the department or location. That seemed like a simple thing to do with my new SNH Data Table from JSON Configuration, and with just a little bit of hackery, I think I could create one configuration script that would work for both departments and locations. This time, instead of starting with the page layout, I decided to start with that multi-purpose configuration script. Here’s the idea: create a new script called RosterConfig that has just one Perspective (All), and then use the State options as indicators of which entity we want (department or location). Here is what I came up with using the Content Selector Configuration Editor:

var RosterConfig = Class.create();
RosterConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'all',
		label: 'All',
		roles: ''
	}],

	state: [{
		name: 'department',
		label: 'Department'
	},{
		name: 'location',
		label: 'Location'
	}],

	table: {
		all: [{
			name: 'sys_user',
			displayName: 'User',
			department: {
				filter: 'active=true^department=javascript:$sp.getParameter("sys_id")',
				fields: 'name,title,email,location',
				btnarray: [],
				refmap: {
					cmn_location: 'location_roster'
				},
				actarray: []
			},
			location: {
				filter: 'active=true^location=javascript:$sp.getParameter("sys_id")',
				fields: 'name,title,department,email',
				btnarray: [],
				refmap: {
					cmn_department: 'department_roster'
				},
				actarray: []
			}
		}]
	},

	type: 'RosterConfig'
});

My plan was to create a department_roster page and a location_roster page, so I mapped the cmn_location table to the location_roster page in the department state and cmn_department table to the department_roster page in the location state. Then I went ahead and built the department_roster page, pulled it up in the Service Portal Designer, and dragged the SNH Data Table from JSON Configuration widget into a full-width container. Using the little pencil icon to bring up the widget options editor for the widget, I entered the name of our new configuration script and set the State to department.

Configuring the custom Data Table widget

I essentially repeated the same process for the location_roster page, but for that page, I set the State to location. Now all that was left to do was to go back into the UserDirectoryConfig script and map the department and location tables to their respective new pages. But before I did that, I wanted to test things out, just to make sure that everything was working as I had intended. Unfortunately, that was not the case. It turns out that my attempt to snag the sys_id off of the URL in the filter was not working. Presumably, the embedded script doesn’t work because the script engine that runs the code does not have access to $sp:

active=true^department=javascript:$sp.getParameter("sys_id")

So, I tried a few more things:

gs.action.getGlideURI().get("sys_id")
RP.getParameterValue("sys_id")
$location.search()["sys_id"]
(... and too many others to list here!)

The bottom line for all of that was that nothing worked. As far as I can tell, by the time that you are running the script in the filter to obtain the value, you have lost touch with anything that might have some kind of relationship with the current URL. So I gave up on the idea of running a script and switched my filter to this:

active=true^department={{sys_id}}

Of course, that doesn’t do anything, either. At least, it didn’t until I added the following lines to my base Data Table widget right after I obtained the filter from its original source:

if (data.filter && data.filter.indexOf('{{sys_id}}')) {
	data.filter = data.filter.replace('{{sys_id}}', $sp.getParameter('sys_id'));
}

I don’t really like doing that, as it is definitely a specialized hack just for this particular circumstance, but it does work, so there is that. The one consolation that I could think of was that sys_id was probably the only thing that I would ever want to snag off of the URL, and I might find some other context in which I might want to do that again, so it was not that use-case specific. Still, I would have preferred to have gotten this to work without having to resort to that.

Once I got over that little hurdle, I decided that I really did not like the page being just the list of users. I wanted to have some kind of context at the top of the page, so I ended up building another little custom widget to sit on top of the data table. Here is the HTML that I came up with for that guy:

<div ng-hide="data.name">
  <div class="alert alert-danger">
    ${Department not found.}
  </div>
</div>
<div ng-show="data.name">
  <div style="text-align: center;">
    <h3 class="text-primary">{{data.name}} ${Department}</h3>
  </div>		
  <div>
    <p>{{data.description}}</p>
    <p>
      <span style="font-weight: bold">${Department Head}</span>
      <br/>
      <span ng-hide="data.dept_head_id"><i>(Vacant)</i></span>
      <span ng-show="data.dept_head_id">
        <sn-avatar primary="data.dept_head_id" class="avatar-smallx" show-presence="true" enable-context-menu="false"></sn-avatar>
        <span style="font-size: medium;">{{data.dept_head}}</span>
      </span>
  </div>		
</div>		

… and here is the server side script:

(function() {
	var deptGR = new GlideRecord('cmn_department');
	if (deptGR.get($sp.getParameter('sys_id'))) {
		data.name = deptGR.getDisplayValue('name');
		data.description = deptGR.getDisplayValue('description');
		data.dept_head = deptGR.getDisplayValue('dept_head');
		data.dept_head_id = deptGR.getValue('dept_head');
	}
})();

Now it looks a little better:

Final Department Roster page

I something similar for the location_roster page, and after that, all that was left was to go back into the original UserDirectoryConfig script and map the department and location tables to their new pages.

Mapping the tables to their respective pages

With that out of the way, now you can bring up the directory, click on a location, click on a department in the location roster, and then click on a user, and since every page includes the dynamic breadcrumbs widget, it all gets tracked at the top of the page.

User Profile Pa

I ended up having to do a little more work than I had originally anticipated with the need to build out the custom department_roster and location_roster pages, but that gave me a chance to utilize my newest customization of the Data Table widget, so it all worked out in the end. If you would like to play around with it on your own instance, here is an Update Set that should contain all of the parts that you need.