Service Portal Form Fields, Corrected

“If you take care of the small things, the big things take care of themselves. You can gain more control over your life by paying closer attention to the little things.”
Emily Dickinson

So, I have had this annoying little bug in my Service Portal Form Fields feature that just wasn’t quite annoying enough to compel me to fix it. Lately, however, I have been doing some testing with a form that included an email field, and I finally got irritated enough to hunt down the problem and put an end to it.

The issue came about when I added the reference type fields. Under the hood, a reference type field is just a form field wrapper around an snRecordPicker. Prior to adding the reference type to the list of supported types, I displayed validation messages only if the field had been $touched or the form had been $submitted. For a reference field, though, the field is never $touched (it’s not even visible on the screen), so I added $dirty as well. That solved my problem for reference fields, but it had the unfortunate side effect of displaying the validation messages while you were still filling out the field on all other types. For fields that are simply required, that’s not a problem (as soon as you start typing, you satisfy the criteria, so there is no validation error). On fields such as email addresses, though, the first character that you type is not a valid email address, so up pops the error message before you even finish entering the data. That’s just annoying!

Anyway, the solution is obviously to only include $dirty on reference fields and leave the others as they were. Unfortunately, that is a generic line that applies all form fields of any type, so I had to includes some conditional logic in there. Here is the offending line of code:

htmlText += "      <div id=\"message." + name + "\" ng-show=\"(" + fullName + ".$touched || " + fullName + ".snhTouched || " + fullName + ".$dirty || " + form + ".$submitted) && " + fullName + ".$invalid\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";

Now, that is a huge, long line of code, so I really did not want to make it any longer by throwing some conditional logic in for the $dirty attribute. I ended up shortening it to this:

htmlText += "      <div id=\"message." + name + "\" ng-show=\"" + errorShow + "\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";

… and then building the value of the new errorShow variable ahead of that with this code:

var errorShow = "(" + fullName + ".$touched || " + fullName + ".snhTouched || ";
if (type == 'reference') {
	errorShow += fullName + ".$dirty || ";
}
errorShow += form + ".$submitted) && " + fullName + ".$invalid";

That was it. Problem solved. It turns out that it was a pretty simple fix, and something that should have been done a long, long time ago. Well, at least it’s done now. Here’s an Update Set for those of you who are into that kind of thing.

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.

Content Selector Configuration Editor, Corrected

“Beware of little expenses; a small leak will sink a great ship.”
Benjamin Franklin

It just seems to be the natural order of things that just when I think I finally wrapped something up and tied a big bow on it, I stumble across yet another fly in the ointment. The other day I was building yet another configuration script using my Content Selector Configuration Editor, but when I tried to use it, I ran into a problem. Since I happened to be working in a Scoped Application, the generated script failed to locate the base class, since that class in the global scope. The problem was this generated line of script:

ScopedAppConfig.prototype = Object.extendsObject(ContentSelectorConfig, {

For a Scoped Application, that line should really be this:

ScopedAppConfig.prototype = Object.extendsObject(global.ContentSelectorConfig, {

It seemed simple enough to fix, but first I had to figure out how to tell if I was in a Scoped App or not. After searching around for a bit, I finally came across this little useful tidbit:

var currentScope = gs.getCurrentApplicationScope();

Once I had a way of figuring out what scope I was in, it was easy enough to change this line:

script += ".prototype = Object.extendsObject(ContentSelectorConfig, {\n";

… to this:

script += ".prototype = Object.extendsObject(";
if (gs.getCurrentApplicationScope() != 'global') {
	script += "global.";			
}
script += "ContentSelectorConfig, {\n";

Once I made the change, I pulled my Scoped Application script up in the Content Selector Configuration Editor, saved it, and tried it again. This time, it worked, which was great. Unfortunately, later on when I went to pull it up in the editor again to make some additional changes, it wasn’t on the drop-down list. It didn’t take long to figure out that the problem was the database table search filter specified in the pick list form field definition:

<snh-form-field
  snh-label="Content Selector Configuration"
  snh-model="c.data.script"
  snh-name="script"
  snh-type="reference"
  snh-help="Select the Content Selector Configuration that you would like to edit."
  snh-change="scriptSelected();"
  placeholder="Choose a Content Selector Configuration"
  table="'sys_script_include'"
  default-query="'active=true^scriptCONTAINSObject.extendsObject(ContentSelectorConfig'"
  display-field="'name'"
  search-fields="'name'"
  value-field="'api_name'"/>

The current filter picks up all of the scripts tht contain “Object.extendsObject(ContentSelectorConfig”, but not those that contained “Object.extendsObject(global.ContentSelectorConfig”. I needed to tweak the filter just a bit to pick up both. I ended up with the following filter value:

active=true^scriptCONTAINSObject.extendsObject(ContentSelectorConfig^ORscriptCONTAINSObject.extendsObject(global.ContentSelectorConfig

… and that took care of that little issue. I’m sure that this will not be the last time that I run into a little issue with this particular product, but for now, here is yet another version stuffed into yet another Update Set. Hopefully, that will be it for the needed corrections for at least a little while!

Service Portal User Directory

“Do. Or do not. There is no try.”
Yoda

I have been playing around a lot lately with my hacked up Service Portal Data Table widget, but just about everything that I have done has been centered around some kind of Task. I have been wanting to do something a little different, so I decided that I would use my breadcrumbs widget, my content selector, and my custom data table to create a User Directory portal page. I started out by creating a new page called user_directory and opening it up in the Portal Designer.

First I dragged a 12-wide container over into the top of the page and then dragged the breadcrumbs widget into that. Then I dragged over a 3/9 container and put the content selector into the narrow portion and the data table into the wider portion. That gave me my basic layout.

user_directory page basic layout

Since this was going to be a list of Users on the portal, I decided that the page to which we will link should be the user_profile portal page, so I updated the table options to link to that page, and to add a user icon as the glyph.

Updating the data table widget options

We also need to update the content selector widget options, but it is going to ask for a configuration script, which we have not built just yet, so let’s do that now. For this, we can use the new Content Selector Configuration Editor to create a new configuration script called UserDirectoryConfig.

Creating a new configuration script using the Content Selector Configuration Editor

For this initial version of the directory, we will have two Perspectives: 1) a general public perspective for all authenticated users, and 2) a user administration perspective for those with the authority to update user records. We will also define 3 States: 1) active users, 2) inactive users, and 3) all users. For each Perspective, we will define a single Table, the User (sys_user) table.

Updating the new configuration script using the Content Selector Configuration Editor

For all of the public states, we will specify the following field list:

name,title,department,location,manager

Since I do not want regular users looking at inactive users, I used the following filter for both the Active state and the All state:

active=true

For the Inactive state, which I wanted to always come up empty, I used this:

active=true^active=false

For the admin states, we will drop the manager and add the user_name, and for the All state, we will also add the active flag:

user_name,name,title,department,location,active

And of course the filters for the admin states are all pretty self explanatory, so there’s no need to waste any space on those here. One little extra thing that I did want to add to all of the admin states, though, was an Edit button. Clicking on the user_name will get you to the user_profile page, but if you want to edit the user information, you need to go the form page, so I added a button for that purpose to all three of the admin states.

Administrative Edit button

After completing the configuration, saving the form produced the following configuration script:

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

	perspective: [{
		name: 'everyone',
		label: 'Public',
		roles: ''
	},{
		name: 'admin',
		label: 'User Admin',
		roles: 'user_admin,admin'
	}],

	state: [{
		name: 'active',
		label: 'Active'
	},{
		name: 'inactive',
		label: 'Inactive'
	},{
		name: 'all',
		label: 'All'
	}],

	table: {
		everyone: [{
			name: 'sys_user',
			displayName: 'User',
			active: {
				filter: 'active=true',
				fields: 'name,title,department,location,manager',
				btnarray: [],
				refmap: {},
				actarray: []
			},
			inactive: {
				filter: 'active=true^active=false',
				fields: 'name,title,department,location,manager',
				btnarray: [],
				refmap: {},
				actarray: []
			},
			all: {
				filter: 'active=true',
				fields: 'name,title,department,location,manager',
				btnarray: [],
				refmap: {},
				actarray: []
			}
		}],
		admin: [{
			name: 'sys_user',
			displayName: 'User',
			active: {
				filter: 'active=true',
				fields: 'user_name,name,title,department,location',
				btnarray: [{
					name: 'edit',
					label: 'Edit',
					heading: 'Edit',
					icon: 'edit',
					color: '',
					hint: '',
					page_id: 'form'
				}],
				refmap: {},
				actarray: []
			},
			inactive: {
				filter: 'active=false',
				fields: 'user_name,name,title,department,location',
				btnarray: [{
					name: 'edit',
					label: 'Edit',
					heading: 'Edit',
					icon: 'edit',
					color: '',
					hint: '',
					page_id: 'form'
				}],
				refmap: {},
				actarray: []
			},
			all: {
				filter: 'true',
				fields: 'user_name,name,title,department,location,active',
				btnarray: [{
					name: 'edit',
					label: 'Edit',
					heading: 'Edit',
					icon: 'edit',
					color: '',
					hint: '',
					page_id: 'form'
				}],
				refmap: {},
				actarray: []
			}
		}]
	},

	type: 'UserDirectoryConfig'
});

Now we can go back into the Page Designer and edit the content selector widget options to specify our new configuration script.

Editing the content selector widget options in the Page Designer

Now all that is left to do is to go out to the Service Portal and give it a go. Let’s see how it comes out.

First look at the new User Directory portal page

I like it! I clicked around and tried a few things. Overall, I think it came out pretty good, but I did come across one flaw already: clicking on someone’s department or location will take you to the user_profile page and get you a nasty error message because there is no user on file with the sys_id of a department or location. That’s not good. That also brings up an interesting point: I don’t really want to send regular users to the form page for departments or locations, either. I need to find a more appropriate destination for those links. I guess I won’t be wrapping this all up in a bow today after all. I’ll have to give this one some thought, so this looks like an issue that we will have to take up next time out.