Collaboration Store, Part V

“Do not be embarrassed by your failures; learn from them and start again.”
Richard Branson

With the completion of the client side code, it is now time to turn our attention to a much bigger effort, all of the things that will need to go on over on the server side. This will involve a number of items beyond just the widget itself, but we can start with the widget and branch out from there. One thing that I know I will need for sure is a Script Include to house all of the various common routines, so I built out an empty shell of that, just to get things started.

var CollaborationStoreUtils = Class.create();
CollaborationStoreUtils.prototype = {
    initialize: function() {
    },

	type: 'CollaborationStoreUtils'
};

That’s enough to reference the script in the widget, which we should do right out of the gate, along with gathering up a couple of our application’s properties and checking to make sure that the set-up process hasn’t already been completed:

var csu = new CollaborationStoreUtils();
data.registeredHost = gs.getProperty('x_11556_col_store.host_instance');
data.registeredHostName = gs.getProperty('x_11556_col_store.store_name');
var thisInstance = gs.getProperty('instance_name');
var mbrGR = new GlideRecord('x_11556_col_store_member_organization');
if (mbrGR.get('instance', thisInstance)) {
	data.phase = 3;
}

We get the instance name from a stock system property (instance_name) and then see if we can fetch a record from the database for that instance. If we can, then the set-up process has already been completed, and we advance the phase to 3 to bring up the set-up completion screen. The next thing that we do is check for input, and if there is input, then we grab the data that we need coming in from the client side and check the input.action variable (c.data.action on the client side) to see what it is that we have been asked to do.

if (input) {
	data.registeredHost = gs.getProperty('x_11556_col_store.host_instance');
	data.registeredHostName = gs.getProperty('x_11556_col_store.store_name');
	data.phase = input.phase;
	data.instance_type = input.instance_type;
	data.host_instance_id = input.host_instance_id;
	data.store_name = input.store_name;
	data.instance_name = input.instance_name;
	data.email = input.email;
	data.description = input.description;
	data.validationError = false;
	if (input.action == 'save') {
		// save logic goes here ...
	} else if (input.action == 'setup') {
		// set-up logic goes here ...
	}
}

That is the basic structure of the widget, but of course, the devil is in the details. Since the save process comes before the set-up process, we’ll take that one on first.

If you elected to set up a Host instance, then there is nothing more to do at this point other than to send out the email verification notice and advance the phase to 2 so that we can collect the value of the code that was sent out and entered by the user. However, if you elected to set up a Client instance, then we have a little bit of further work to do before we proceed. For one thing, we need to make sure that you did not specify your own instance name as the host instance, as you cannot be a client of your own host. Assuming that we passed that little check, the next thing that we need to do is to check to see if the host that you specified is, in fact, an actual Collaboration Store host. That will take a bit of REST API work, but for today, we will assume that there is a function in our Script Include that can make that call. To complete the save action, we can also assume that there is another Script Include function that handles the sending out of the Notification, which will allow us to wrap up the save action logic as far as the widget is concerned.

if (data.instance_type == 'client') {
	if (data.host_instance_id == thisInstance) {
		gs.addErrorMessage('You cannot specify your own instance as the host instance');
		data.validationError = true;
	} else {
		var resp = csu.getStoreInfo(data.host_instance_id);
		if (resp.responseCode == '200' && resp.name > '') {
			data.store_name = resp.name;
			data.store_info = resp.storeInfo.result.info;
		} else {
			gs.addErrorMessage(data.host_instance_id + ' is not a valid Collaboration Store instance');
			data.validationError = true;
		}
	}
}
if (!data.validationError) {
	data.oneTimeCode = csu.verifyInstanceEmail(data.email);
}

So now we have referenced two nonexistent Script Include functions to get through this section of code. We should build those out next, just to complete things, but neither one is an independent function. The getStoreInfo function needs to call a REST service, which also doesn’t exist, and the verifyInstanceEmail function needs to trigger a notification, which does not exist at this point, either. We should create those underlying services first, and make sure that they work, and then we can build the Script Include functions that invoke them to finish things up.

That seems like quite a bit of work in and of itself, so this looks like a good place to wrap things up for now. We can jump on that initial web service first thing next time out.

Collaboration Store, Part IV

“Thinking is easy, acting is difficult, and to put one’s thoughts into action is the most difficult thing in the world.”
Johann Wolfgang von Goethe

Now that we have the visual portion of our widget all laid out the way that we want it, it’s time to crawl under the hood and start laying down some code that will make it all work. The easiest pace to start on that would be client side code. There isn’t that much of it, since the bulk of the action will take place on the server side, and it is all pretty basic standard stuff. For the most part, the client side code is just there to handle the various button clicks and to kick things over to the server side for processing.

The first button click to handle will be the save buttons on the initial data entry screen. There are actually two of them, but only one appears on the screen at any given time, and they invoke the same client side function. Here it is:

$scope.save = function() {
	c.data.phase = 0;
	c.data.action = 'save';
	c.server.update().then(function(response) {
		if (response.oneTimeCode) {
			c.data.oneTimeCode = response.oneTimeCode;
			c.data.phase = 2;
		} else {
			c.data.phase = 1;
		}
	});
};

Before throwing things to the server side for processing, we first set the phase to 0 and the action to save. This will control the logic flow on the server side, where we do all of the actual work. Once the server side code has completed, we check for the presence of a one-time code in the response, which basically tells us that all went well and now we are in the process of verifying the email address. If we don’t get a one-time code, then something went off of the rails, and we kick the phase back to 1 to start the process all over again.

The next button click to handle is the email verification, which we can do right here on the client side, as we are now in possession of the one-time code and we can compare that to the code entered on the screen. If the codes match, the we advance things to the set-up process, and if not, we just leave things as they are until the correct code is entered or the process is cancelled.

$scope.verify = function() {
	if (c.data.security_code == c.data.oneTimeCode) {
		c.data.phase = 0;
		c.data.action = 'setup';
		c.server.update().then(function(response) {
			if (response.validationError) {
				c.data.phase = 1;
			} else {
				c.data.phase = 3;
			}
		});
	}
};

It is still possible at this point for things to go south, so we have to check for that as well, and once again, cycle things back to phase 1 if there is a problem. Otherwise, we advance to phase 3, which simply hides the email verification screen and reveals the set-up completion confirmation screen.

The last thing that we need to handle on the client side is the Cancel button. I just stole this code from an earlier project, and it just returns you to the home page of the main UI if you are in the main UI, or returns you to the home page of the portal, if you are running in the Service Portal.

$scope.cancel = function() {
	var destination = '?';
	if (window.parent.location.href != window.location.href) {
		destination = '/home.do';
	}
	window.location.href = destination;
};

The only other code on the client side is a simple check to see if the server set an initial phase, which can happen if you try to run the set-up process more than once. Here is the entire client side script, with everything included:

api.controller=function($scope) {
	var c = this;

	c.data.phase = c.data.phase || 1;

	$scope.save = function() {
		c.data.phase = 0;
		c.data.action = 'save';
		c.server.update().then(function(response) {
			if (response.oneTimeCode) {
				c.data.oneTimeCode = response.oneTimeCode;
				c.data.phase = 2;
			} else {
				c.data.phase = 1;
			}
		});
	};

	$scope.verify = function() {
		if (c.data.security_code == c.data.oneTimeCode) {
			c.data.phase = 0;
			c.data.action = 'setup';
			c.server.update().then(function(response) {
				if (response.validationError) {
					c.data.phase = 1;
				} else {
					c.data.phase = 3;
				}
			});
		}
	};

	$scope.cancel = function() {
		var destination = '?';
		if (window.parent.location.href != window.location.href) {
			destination = '/home.do';
		}
		window.location.href = destination;
	};
};

That takes care of the easy part. All of the real work is over on the server side, and there is actually a lot that needs to go on over there, and much of it outside of the scope of the widget itself. That is undoubtedly a multi-installment activity, but we will start taking that on piece by piece next time out.

Collaboration Store, Part III

“It is not enough to do your best: you must know what to do, and then do your best.”
W. Edwards Deming

Today we will build the widget for the initial set-up process for the Collaboration Store app. I always like to start with the visual portion and lay things out on the screen the way that I want to see them, but before we get into that, I should explain a little bit about my basic concept for the set-up process. There are actually three independent screens involved: 1) the initial screen where you enter all of the data about your installation, 2) an email verification screen where you enter a code that was sent to your email address to verify your access to the email account, and 3) a final completion screen that lets you know that you are all set up. The HTML for the widget will include all three screens, and I will use ng-show attributes to control which section is visible at any given stage of the process. Within the widget, I will refer to that as the phase, and set up a variable called c.data.phase to track the progress through the screens.

Here is what the initial data entry screen looks like:

Initial set-up data entry screen

… and here is the HTML for that initial (phase 1) screen:

<div class="row" ng-show="c.data.phase == 1">
  <div style="text-align: center;">
    <h3>${Collaboration Store Set-up}</h3>
  </div>
  <div>
    <p>
      Welcome to the Collaboration Store set-up process.
      There are two ways that you can set up the Collaboration Store on your instance:
      1) you can be the Host Instance to which all other instances connect, or
      2) you can connect to an existing Collaboration Store with their permission.
      To become the Host Instance of your own Collaboration Store, select <em>This instance will be
      the Host of the store</em> from the Installation Type choices below.
      If you are not the Host Instance, then you will need to provide the Instance ID of the
      Collaboration Store to which you would like to connect.
    </p>
  </div>
  <form id="form1" name="form1" novalidate>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.instance_type"
          snh-name="instance_type"
          snh-label="Installation Type"
          snh-type="select"
          snh-required="true"
          snh-choices='[{"value":"host", "label":"This instance will be the Host of the store"},
                        {"value":"client", "label":"This instance will connect to an existing store"}]'/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.host_instance_id"
          snh-name="host_instance_id"
          snh-label="Host Instance ID"
          snh-required="c.data.instance_type == 'client'"
          ng-show="c.data.instance_type == 'client'"/>
        <snh-form-field
          snh-model="c.data.store_name"
          snh-name="store_name"
          snh-label="Store Name"
          snh-required="c.data.instance_type == 'host'"
          ng-show="c.data.instance_type == 'host'"/>
      </div>
    </div>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.instance_name"
          snh-name="instance_name"
          snh-label="Instance Display Name"
          snh-required="true"/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.email"
          snh-name="email"
          snh-label="Email"
          snh-type="email"
          snh-required="true"/>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.description"
          snh-name="description"
          snh-label="Instance Description"
          snh-type="textarea"
          snh-required="true"/>
      </div>
    </div>
  </form>
  <div class="row">
    <div class="col-sm-12" style="text-align: center;">
      <button class="btn btn-primary" ng-disabled="!(form1.$valid)" ng-show="c.data.instance_type == 'host'" ng-click="save();">${Create New Collaboration Store}</button>
      <button class="btn btn-primary" ng-disabled="!(form1.$valid)" ng-show="c.data.instance_type == 'client'" ng-click="save();">${Complete Set-up and Request Access}</button>
    </div>
  </div>
</div>

Basically, this is just a standard HTML form full of snh-form-fields organized into rows and columns. There are a couple of fields and a couple of buttons that are controlled by the value of that first SELECT, but other than that, it is pretty standard stuff, and there is no reason to get into any of that here.

The screen for the 2nd phase is much simpler, with only a single data entry field used to collect the code that was sent out in a Notification (more on that later) to verify the email address.

Email verification data entry screen

… and here is the HTML for the phase 2 screen:

<div class="row" ng-show="c.data.phase == 2">
  <div style="text-align: center;">
    <h3>${Email Verification}</h3>
  </div>
  <div>
    <p>
      A verification email has been sent to {{c.data.email}} with a one-time security code.
      Please enter the code below to continue.
    </p>
    <p>
      Cancelling this process will terminate the set-up process.
    </p>
  </div>
  <form id="form2" name="form2" novalidate>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.security_code"
          snh-name="security_code"
          snh-label="Security Code"
          snh-required="true"
          placeholder="Enter the security code sent to you via email"/>
      </div>
    </div>
  </form>
  <div class="row">
    <div class="col-sm-12" style="text-align: center;">
      <button class="btn btn-default" ng-disabled="!(form2.$valid)" ng-click="cancel();">${Cancel}</button>
      <button class="btn btn-primary" ng-disabled="!(form2.$valid)" ng-click="verify();">${Submit Verification Code}</button>
    </div>
  </div>
</div>

The screen for the 3rd phase is even simpler, with no data entry fields at all. It is just a message indicating that set-up is complete and was successful.

Set-up completion screen

… and here is the HTML for the phase 3 screen:

<div class="row" ng-show="c.data.phase == 3">
  <div style="text-align: center;">
    <h3>${Set Up Complete!}</h3>
  </div>
  <div>
    <p>${Congratulations!}</p>
    <p>
      The Collaboration Store set-up is now complete. Your instance has been successfully registered with the
      <b class="text-primary">{{c.data.registeredHostName}}</b> ({{c.data.registeredHost}})
      Host and is now ready to utilize the Collaboration Store features.
    </p>
  </div>
</div>

That pretty much takes care of the visual portion of the widget, which is usually the easiest part. Now we have to put all of the code underneath to make everything do all of the things that we want to do during the set-up process. One of the first things that we will want to do is to make sure that the set-up process has not already been completed. We should be able to tell that right off the bat by looking to see if there is a record in the table for the instance. If there is, then things have already been set up, and so we can artificially advance things to phase 3 and put up that final screen in the event that someone tries to go through the process a second time.

Assuming that this is the first time through, though, we will want to validate the data, and assuming that all goes well, we will want to send out the notification with the random code and advance the phase so that we can verify the email address. Once that’s done, we will need to update the database, and in the case of a client instance, we will need to register the instance with the host. To make all of that work, we will need some REST services, and probably a Script Include to contain some code to handle both sides of those inter-instance communications. We are definitely not going to get through all of that in a single installment, but we can take them all on, one issue at a time, as we work our way through all of the parts and pieces that will need to be built. Probably the easiest thing to tackle next would be the client-side code of the widget, so let’s start with that next time out.

Collaboration Store, Part II

“Tell me and I forget. Teach me and I remember. Involve me and I learn.”
Benjamin Franklin

Now that I have gone and thrown the idea out there for all to see, it’s time to get to work and see if I can actually pull this off. To begin, I need to set up the Scoped Application, which is basically a repeat of what I went through to set up the Scoped Application for my little Webhooks project, so there is no need to repeat all of that here. Here is how it came out:

Initial Collaboration Store Scoped Application

With that out of the way, the next order of business is to create that first table in which to store all of the instances. Again, that is pretty standard stuff and not really worthy of a step by step walk-through of the process, but here is the associated form, which will give you an idea of the columns that I have selected at this point in the process:

Member Organization table input form

Now we have a place to store the information on the participating instances, so it’s time to build the initial set-up process that will populate this table. Before we dive into that, though, I should mention that when I set up the application, I also set up a few System Properties using the UI Action that I created for that purpose a couple of years ago.

UI Action to set up System Properties for a Scoped Application

That’s been a handy little tool that does a number of things under the hood, but we don’t need to get into all of that here. If you are interested in that for any reason, you can grab an Update Set from here. For this phase of the project, I came up with three properties that I think will be needed in order to do what I would like to do. That may change over time as I get more into the weeds, but for now, here is the list:

Initial System Properties for the Collaboration Store application

That should give us all of the artifacts that we should need to start working on the initial set-up process. As you might have noticed in the above screen shot, I have already created a menu item to launch the initial set-up process from the navigation side bar. Right now, you can also see some of the other menu options for the app, but sometime before things are ready for prime time, my plan is to make all other menu items inactive, and then once the set-up process has been completed, a final step in the process would activate all of the others and inactivate the set-up menu item. For now, though, you can see everything, and it will probably be that way for some time until we get much closer to the end of things.

As for the set-up process itself, there are a number of different ways to go here. I could build something the main UI, where the primary technology is Apache Jelly. I could also build a Service Portal widget, where the primary technology is AngularJS. Both of those are considered Old School at this point, though, and all of the cool kids are now using the Now® Experience UI Framework and the ui-component extension for application development. While that seems like the appropriate way to go, my personal skill set does not yet include mastery of that particular technology, and I don’t really feel like this project would be a good place to address that particular shortcoming in my technical expertise. Since the initial set-up is just one small part of this effort, I am going to take the easy way out and just build a simple widget.

Building a brand new widget starting with a blank canvas is a little bit of a project, though, so that seems like something worthy of its own dedicated installment. Rather than start on that here, let’s take that up next time out.

Collaboration Store

“Don’t worry about people stealing your ideas. If your ideas are any good, you’ll have to ram them down people’s throats.”
Howard Aiken

So I have had this idea percolating in the back of my brain for a while, but other than a few false starts, I haven’t ever done much with it. I have considered a number of different approaches, and have gone back and forth on the best way to tackle one thing or another, but other than mulling things over inside of my head, I have never really attempted to build anything out. I still don’t have a full understanding of how I am going to accomplish certain things, but that’s never really stopped me before from just plowing ahead with what I know and figuring the rest out along the way. Like most things, the most important thing of all is to just get started, and the rest will all work itself out over time. As they used to say in the old Nike commercial: Just Do It!

It’s a pretty simple idea, really. I want to build something like the ServiceNow Store or the developer’s Share site, only for a limited consortium of instances. It might be a collection of PDIs or an industry group or maybe just a couple of independent instances in the same organization, but the idea is to have a way to share stuff amongst a private group that are not otherwise connected in the way that one customer’s multiple instances are connected via their own internal store. I’ve gone back and forth on whether it would be better to do this peer to peer, with no central host, or to designate one of the instances as the master and have all of the others communicating through that one and not directly with each other. There are issues and benefits with each approach, and I really like the idea of having no single instance in charge, but after mulling over all of the various challenges, I think having a host instance would be the easiest approach, so I am going to make my first attempt using that strategy.

At this point, I don’t have all of the details worked out, but I have a rough idea of what I would like to accomplish. The first order of business would seem to be to get the instances talking to one another, which I plan to do using the built-in REST capabilities of the Now Platform. Some of the transactions will have to be built using the Scripted REST API, but hopefully most of them will just use the standard, out-of-the-box capability. My plan is to create a Scoped Application with a set-up process where you will either elect to set up a Host Instance, or provide the instance name of the Host Instance to which you would like to connect. The I am the Host option would be the simplest to code; the other would require communication with the specified host and getting your instance registered with the Host Instance. Also, any time a new instance was registered with the group, all of the other instances should be notified of the new member of group. Things now start to get a little complicated here, and this is just the initial set-up! As I said, I don’t have all of the details worked out just yet, but I do have the basic idea, so I just need to get started and see how things start to come out.

To keep track of the instances in the collective, I will need to set up a table that will contain all of the details for each instance. It shouldn’t be too much data; maybe just the instance name, a display name, a description, and some control data like an active flag or an activation date of some kind. Also, if I want keep track of activities related to each member instance, I might also want an activity log table as well. I like the idea of having that information, but I may save that feature for a later time once I get all of this other stuff worked out the way that it needs to be.

There are a bunch of other considerations such as Roles, ACLs, and web-only service accounts to make all of this work, but again, that’s all in the details. Things should definitely be secured, and I will definitely want to do that, but my usual experience with Security is trying to get around it. It will be interesting to spend a little time on the other side of the fence. But that is not my first priority. Initially, I just want to get something to work. Once we cross that bridge, then I will circle back and make sure that everything is locked down in the way that it should be. I can only deal with one thing at a time, so to kick this thing off, I am just going to try to build out the initial set-up process. That’s complex enough all by itself. And it will probably take a little time, just to get that tuned up the way that I want it to work.

So, that’s the idea, anyway, and today was just about throwing the idea out there for all to see. Next time out, I will start putting the pieces together to see if we can’t turn the idea into a real, functioning scoped application.

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.

Customizing the Data Table Widget, Again

“Work never killed anyone. It’s worry that does the damage. And the worry would disappear if we’d just settle down and do the work.”
Earl Nightingale

It’s been a while since I first set out to build a custom version of the stock Data Table widget, but since that time, I have used my hacked up version for a number of different projects, including sharing pages with my companion widget, the Configurable Data Table Widget Content Selector. Now that I have a way to edit the configuration scripts for the content selector, that has become my primary method for setting up the parameters for a data table. The content selector, though, was designed to give the end user the ability to select different Perspectives, different States, and different Tables. That’s a nice feature, but there are times when I just want to display a table of data without any options to look at any other data. I thought about setting up an option to make the content selector hidden for those instances, but then it occurred to me that the better approach was to cut out the middleman entirely and create a version of the Data Table widget that read the configuration script directly. This way, I wouldn’t have to put the content selector on the page at all.

So I cloned my SNH Data Table from URL Definition widget to create a new SNH Data Table from JSON Configuration widget. Then I opened up my Content Selector widget and started stealing parts and pieces of that guy and pasting them into my new Data Table widget, starting with the widget option for the name of the configuration script:

{
      "hint":"Mandatory configuration script that is an extension of ContentSelectorConfig",
     "name":"configuration_script",
     "section":"Behavior",
     "label":"Configuration Script",
     "type":"string"
}

I also threw in three new options so that you could override the default Perspective, Table, and State values.

{
   "hint":"Optional override of the default Perspective",
   "name":"perspective",
   "section":"Behavior",
   "label":"Perspective",
   "type":"string"
},{
   "hint":"Optional override of the default Table",
   "name":"table",
   "section":"Behavior",
   "label":"Table",
   "type":"string"
},{
   "hint":"Optional override of the default State",
   "name":"state",
   "section":"Behavior",
   "label":"State",
   "type":"string"
}

In the HTML section, I copied in the two warning messages and pasted them in with minimal modifications:

<div ng-hide="options && options.configuration_script">
  <div class="alert alert-danger">
    ${You must specify a configuration script using the widget option editor}
  </div>
</div>
<div ng-show="options && options.configuration_script && !data.config.defaults">
  <div class="alert alert-danger">
    {{options.configuration_script}} ${is not a valid Script Include}
  </div>
</div>

On the server side, I deleted the first several lines of code that dealt with grabbing the table and view from the URL and making sure that something was there, and replaced it with some code that I pretty much lifted intact from the content selector widget. Here is the code that I removed:

deleteOptions(['table','field_list','filter','order_by', 'order_direction','order','maximum_entries']);
if (input) {
	data.table = input.table;
	data.view = input.view;
} else {
	data.table = $sp.getParameter('table') || $sp.getParameter('t');
	data.view = $sp.getParameter('view');
}

if (!data.table) {
	data.invalid_table = true;
	data.table_label = "";
	return;
}

… and here is what I replaced it with:

data.config = {};
data.user = {sys_id: gs.getUserID(), name: gs.getUserName()};
if (options) {
	if (options.configuration_script) {
		var instantiator = new Instantiator(this);
		var configurator = instantiator.getInstance(options.configuration_script);
		if (configurator != null) {
			data.config = configurator.getConfig($sp);
			data.config.authorizedPerspective = getAuthorizedPerspectives();
			establishDefaults(options.perspective, options.table, options.state);
		}
	}
}

if (data.config.defaults && data.config.defaults.perspective && data.config.defaults.table && data.config.defaults.state) {
	var tableList = data.config.table[data.config.defaults.perspective];
	var tbl = -1;
	for (var i in tableList) {
		if (tableList[i].name == data.config.defaults.table) {
			tbl = i;
		}
	}
	data.tableData = tableList[tbl][data.config.defaults.state];
	data.table = data.config.defaults.table;
} else {
	data.invalid_table = true;
	data.table_label = "";
	return;
}

I also grabbed a couple of the functions that were called from there and pasted those in down at the bottom:

function getAuthorizedPerspectives() {
	var authorizedPerspective = [];
	for (var i in data.config.perspective) {
		var p = data.config.perspective[i];
		if (p.roles) {
			var role = p.roles.split(',');
			var authorized = false;
			for (var ii in role) {
				if (gs.hasRole(role[ii])) {
					authorized = true;
				}
			}
			if (authorized) {
				authorizedPerspective.push(p);
			}
		} else {
			authorizedPerspective.push(p);
		}
	}
	return authorizedPerspective;
}

function establishDefaults(perspective, table, state) {
	data.config.defaults = {};
	var p = data.config.authorizedPerspective[0].name;
	if (perspective) {
		if (data.config.table[perspective]) {
			p = perspective;
		}
	}
	if (p) {
		data.config.defaults.perspective = p;
		for (var t in data.config.table[p]) {
			if (!data.config.defaults.table) {
				data.config.defaults.table = data.config.table[p][t].name;
			}
		}
		if (table) {
			for (var t1 in data.config.table[p]) {
			if (data.config.table[p][t1].name == table) {
					data.config.defaults.table = table;
				}
			}
		}
		data.config.defaults.state = data.config.state[0].name;
		if (state) {
			for (var s in data.config.state) {
				if (data.config.state[s].name == state) {
					data.config.defaults.state  = state;
				}
			}
		}
	}
}

I also reworked the area labeled widget parameters to get the data from the configuration instead of the URL. That area now looks like this:

// widget parameters
data.table_label = gr.getLabel();
data.filter = data.tableData.filter;
data.fields = data.tableData.fields;
data.btnarray = data.tableData.btnarray;
data.refmap = data.tableData.refmap;
data.actarray = data.tableData.actarray;
copyParameters(data, ['p', 'o', 'd', 'relationship_id', 'apply_to', 'apply_to_sys_id']);
data.filterACLs = true;
data.show_keywords  = true;
data.fromJSON = true;
data.headerTitle = (options.use_instance_title == "true") ? options.title : gr.getPlural();
data.enable_filter = options.enable_filter;
data.show_new = options.show_new;
data.show_breadcrumbs = options.show_breadcrumbs;
data.table_name = data.table;
data.dataTableWidget = $sp.getWidget('snh-data-table', data);

No modifications were needed on the client side, so now I just needed to create a new test page, drag the widget onto the page and then use the little pencil icon to configure the widget. I called my new page table_from_json and pulled it up in the Page Designer to drag in this new widget. Using the widget option editor, I entered the name of the Script Include that we have been playing around with lately and left all of the other options that I added blank for this first test.

SNH Data Table from JSON Configuration widget option editor

With that saved, all that was left to do was to go out to the Service Portal and bring up the page.

SNH Data Table from JSON Configuration widget on the new test page

Not bad! I played around with it for a while, trying out different options using the widget option editor in the Page Designer, and everything seems like it all works OK. I’m sure that I’ve hidden some kind of error deep in there somewhere that will come out one day, but so far, I have not stumbled across it in any of my feeble testing. For those of you who like to play along at home, here is an Update Set that I am hoping contains all of the needed parts.