SNH Data Table Widgets on Share

“The first time you do a thing is always exciting.”
Agatha Christie

I’ve never used Share before, but after completing the work on the Aggregate List Columns and bundling that work up with all of the other related projects and artifacts, I decided that I would go ahead and post the whole thing out there. I have always hesitated to throw stuff from here out there, mainly because most of the things that you will find on this site are not very well documented, at least not from the user’s perspective. But, I have considered doing it anyway on a number of occasions. I was pretty close to sharing the My Delegates Widget until I discovered that someone else had already beat me to it. I also thought about tossing out a number of other items such as the Dynamic Service Portal Breadcrumbs and the Service Portal Widget Help, but like quite a number of other things, those were just thoughts that never turned into any kind of action. This time, though, quite a number of things were all bundled together into a single Update Set, and I thought that maybe there just might be a thing or two buried in there somewhere that someone somewhere might find to be of value. We’ll see.

My other hesitation to posting this on Share was the fact that these are all Service Portal components, and ServiceNow has made it pretty clear that they would like to see folks abandon the Service Portal in favor of their latest approach to application development. While it may be true that the Service Portal is on the way out, it has been my experience that such transitions usually take some time to be fully realized, so there still may be an active Service Portal or two floating around out there for a while. Still, everyone always likes to jump on the new stuff, so the interest in Service Portal components is something that is bound to start dropping off over time. On the other hand, that actually serves as an argument for shoving it out there now, as waiting around would just mean even less relevance to the environment of the future.

Anyway, it’s done now. Share obviously has a much broader reach than this little blog, so it will be interesting to see if anyone happens to come across it out there with all of the other artifacts on the site. I did take a quick peek this morning, and it does look like a couple of brave souls have already hit the download button, but I don’t see any feedback posted as yet. That will probably take a little more time. Who knows; if it all works out, maybe one day I will throw something else out there. Only time will tell.

Aggregate List Columns, Part IX

“You have to finish things — that’s what you learn from, you learn by finishing things.”
Neil Gaiman

Last time, we attempted to wrap this whole thing up with the remaining modifications to the Content Selector Configuration Editor, but we ran into a problem with the code that we borrowed from the sn-record-picker Helper. Now that we have taken a quick detour to resolve that issue, we need to get back to our new pop-up aggregate column editor and apply the same fix to our pilfered code. As with the sn-record-picker Helper, we need to add some code to the server side to leverage the TableUtils object to get our list of tables in the extension hierarchy. Here is the new Server script, with basically the same function as the one that we added to the sn-record-picker Helper, with a few minor modifications due to the difference in our variable names.

(function() {
	if (input && input.tableName) {
		var tu = new TableUtils(input.tableName);
		var tableList = tu.getTables();
		data.tableList = j2js(tableList);
	}
})();

With that now in place, we can update the client-side buildFieldFilter function to call the server side to get the list, and then use that list to build the new filter.

$scope.buildFieldFilter = function() {
	c.data.fieldFilter = 'name=' + c.widget.options.shared.table.value;
	c.data.tableName = c.widget.options.shared.table.value;
	c.server.update().then(function(response) {
		if (response.tableList && Array.isArray(response.tableList) && response.tableList.length > 1) {
			c.data.fieldFilter = 'nameIN' + response.tableList.join(',');
		}
		c.data.fieldFilter += '^elementISNOTEMPTY^internal_type=reference';
	});
};

Now we just need to pop up the editor again and see if that actually fixes the problem that we were having earlier.

Pop-up aggregate column spec editor with field pick list corrected

That’s better! Now when I search for a field on the Incident table, I get to select from all of the fields on the table, not just the ones attached to the primary table. That’s what we wanted to see.

That takes care of the pop-up aggregate column specification editor that we cloned from the existing button/icon specification editor, so now all that is left for us to do is to tweak the code that actually saves the changes once all of the editing has been completed. The Save process actually rebuilds the entire script stored in the Script Include record, so we just need to add some code to create the aggregate column section for each table definition. Once again, we can leverage the existing buttons and icons code as a starting point, and then make the necessary changes to adapt it to use for aggregate columns. Here are the relevant lines from the Save() function in the widget’s Server script:

script += "',\n				btnarray: [";
var lastSeparator = '';
for (var b=0; b<tableTable[tableState.name].btnarray.length; b++) {
	var thisButton = tableTable[tableState.name].btnarray[b];
	script += lastSeparator;
	script += "{\n					name: '";
	script += thisButton.name;
	script += "',\n					label: '";
	script += thisButton.label;
	script += "',\n					heading: '";
	script += thisButton.heading;
	script += "',\n					icon: '";
	script += thisButton.icon;
	script += "',\n					color: '";
	script += thisButton.color;
	script += "',\n					hint: '";
	script += thisButton.hint;
	script += "',\n					page_id: '";
	script += thisButton.page_id;
	script += "'\n				}";
	lastSeparator = ",";
}
script += "]";

As we have done a number of times now, we can make a few global text replacements and alter a few variable names to align with our needs and come up with something that will work for aggregate columns specifications in very much the same way that it is currently working for buttons and icons.

script += "',\n				aggarray: [";
var lastSeparator = '';
for (var g=0; g<tableTable[tableState.name].aggarray.length; g++) {
	var thisAggregate = tableTable[tableState.name].aggarray[g];
	script += lastSeparator;
	script += "{\n					name: '";
	script += thisAggregate.name;
	script += "',\n					label: '";
	script += thisAggregate.label;
	script += "',\n					heading: '";
	script += thisAggregate.heading;
	script += "',\n					table: '";
	script += thisAggregate.table;
	script += "',\n					field: '";
	script += thisAggregate.field;
	script += "',\n					filter: '";
	script += thisAggregate.filter;
	script += "',\n					source: '";
	script += thisAggregate.source;
	script += "'\n				}";
	lastSeparator = ",";
}
script += "]";

And that’s all there is to that. That should be everything now, so all that is left to do is to bundle all of this into an Update Set so that folks can play along at home. This effort has been primarily focused on the components of the Customizing the Data Table Widget project, but it has also involved elements of the Configurable Data Table Widget Content Selector series as well as the Content Selector Configuration Editor. Additionally, many of the Service Portal pages that use these components, such as the Service Portal User Directory, also include the Dynamic Service Portal Breadcrumbs widget. Rather than create a new version for each and every one of these interrelated project, I think I will just lump everything together into a single Update Set and call it version 2.0 of the Customizing the Data Table Widget project. Since many of the widgets involved also utilize the Service Portal Form Fields, that will get pulled into the Update Set as well, and just for good measure, I think I will toss in the sn-record-picker Helper, too. That one is not actually directly related to all of the others, but we did steal some code from there, so there might be a few folks who may want to take a look at that one. You can download the whole lot from here. As always, if you have any comments, questions, concerns, or issues, please leave a comment below. All feedback is always welcome. And if you have made it this far, thanks for following along all the way to the end!

But wait … there’s more!

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.

Fun with Webhooks, Part X

“Control is for beginners.”
Ane Størmer

I’ve been playing around with our little Incident Webhook subsystem to make sure that everything works, and to make sure that I had finally developed all of the pieces that I had intended to build. For the most part, I’m quite happy with what we have put together during this exercise, but like most end users who finally get their hands on something that they have ordered, now that I have a working model in my hands and have tried to use if for various things, I can envision a number of different enhancements that would make things even better. Still, what we have is pretty nice all on its own, although I did break down and make just a few minor adjustments.

One thing that I had thought about doing, but didn’t, was to skip the confirmation pop-up on the custom Webhook Registry page’s Cancel button when no changes had been made to the form. Going through that a few times was enough to motivate me to put that in there, and I like this version much better. While I was in there, I also built a goBack() function to house the code for returning to the previous page, and then called that function wherever it was appropriate. This didn’t really save that much in the way of code, since the current goBack() logic is only one line itself, but it consolidates the logic in a single place if I ever want to wire in support for something like my Dynamic Breadcrumbs. The entire client side code for the Webhook Registry widget now looks like this:

function WebhookRegistry($scope, $location, spModal) {
	var c = this;

	$scope.cancel = function() {
		if ($scope.form1.$dirty) {
			spModal.confirm('Abandond your changes and return to your Webhooks?').then(function(confirmed) {
				if (confirmed) {
					goBack();
				}
			});
		} else {
			goBack();
		}
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			c.server.update().then(function(response) {
				goBack();
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	function goBack() {
		$location.search('id=my_webhooks');
	}
}

One other thing that I noticed when attempting to integrate with various other targets is that many sites are looking for a property named text as opposed to message. I ended up renaming my message field to text to be more compatible with this convention, but it would really be nice to be able to pick and chose what properties you would like to have in your payload, as well as being able to specify what you wanted them to be named. That’s on my wish list for a future version for sure.

Something that I meant to include in this version, but forgot to do, was to emulate the Test URL UI Action on the Webhook Registry widget so that Service Portal users could have that same capability on that portal page. That was definitely on my plan to include, but I just spaced it out when I was putting that all together. I definitely want to be sure to include that at some point in the near future. I would do it now, but I already built the Update Set and I’m just too lazy to go back and fix it now.

One other thing that is on my wish list for some future version is the ability to set this up for more than just the Incident table. I thought about just switching over to the Task table, which includes Incident as well as quite a few other things derived from Task, but the base Task table does not include the Incident’s Caller or the Request’s Requested for, so there would have to be some special considerations included to cover that. The Task table has Opened by, but that’s not really the same thing when you are dealing with folks calling in and dealing with an Agent entering their information. I thought about adding some additional complexity to cover that, but in the end I just put all of that on my One Day … list and left well enough alone.

Based on what I first set out to do, I think it all came out OK, though. Yes, there are quite a few more things that we could add to make it applicable to a broader domain, and there are a number of things that we could do to make it more flexible, user-friendly, and user-customizable, but it’s a decent start. Certainly good enough to warrant the release of an initial version, which you can download here. Since this is a scoped app, I did not bundle any of the dependencies in the Update Set, so if you want to try this out in your own instance as is, you will need to also grab the latest version of SNH Form Fields and SNH ServiceNow Events, which you can find here. All in all, I am happy with the way that it came out, but I am also looking forward to making it even better one day, after I have spent some time attempting to use it as it is today.

Update: There is a better (improved) version here.

Dynamic Service Portal Breadcrumbs, Corrected (again!)

“What we see depends mainly on what we look for.”
Sir John Lubbock

One of the nice things about sharing your code with others is they end up testing it for you, and they do it from a different perspective than your own. The other day I received a comment from Ken out in California regarding my Dynamic Service Portal Breadcrumbs widget. He wanted to let me know that whenever he dragged the widget onto a page in the Portal Page Designer, the widget would disappear. I’ve had that little widget for quite a while now, and I have corrected it, perfected it, enhanced it, and I was actually aware of that behavior, but I had always just chalked that up as an irritating annoyance. From Ken’s perspective, though, it was a problem that needed to be addressed, and of course, he is correct. So, I decided to see if I could figure out why it was doing that, and if I could actually fix the problem.

I started out by doing a little research, just to see if anyone else had experienced this phenomenon. Sure enough, other people had reported the same behavior with other widgets, and the common thread always seemed to be that there was some coding error in the Client Script of the widget. Armed with that little tidbit of information, I set out to do a little experimenting.

The first thing that I did was comment out all of the code inside of the Client Script function and then run over to the Page Designer to see if that had any effect. Sure enough, the widget now appeared on the screen. That told me that I was on the right track, and that the issue actually was some problem in the Client Script. After that, it was just a matter of running through a sort of binary search, revealing and commenting out various chunks of code until I finally narrowed it down to the one line that was causing the problem:

var portalSuffix = ' - ' + $rootScope.portal.title.trim();

The reason that this worked on all of the pages in all of the portals where I have used it, but not in the Page Designer itself, was that the Page Designer has no title defined. When you try to run the trim() function on a String that doesn’t exist, you are going to get a null pointer exception. That’s obviously not good. So now what?

To step back just a bit, the whole point in grabbing the title was to look for it in the page title and remove it. Page titles in the form of <page title> – <portal title> are extra wide and contain the redundant portal title value, so I wanted to strip off that portion, if it was present. If there is no portal title, then that entire segment of code has no value, so I ended up checking for the presence of the title first, and then tucking all of that code underneath that conditional.

if ($rootScope.portal && $rootScope.portal.title) {
	var portalSuffix = ' - ' + $rootScope.portal.title.trim();
	var cutoff = thisTitle.indexOf(portalSuffix);
	if (cutoff != -1) {
		thisTitle = thisTitle.substring(0, cutoff);
	}
}

That was it. Problem solved. Thank you, Ken, for prodding me to take a look at that. It turned out to be a relatively easy fix, and something that I should have addressed a long time ago. Here’s an Update Set with the corrected code.

Dynamic Service Portal Breadcrumbs, Enhanced, Part II

“There are three kinds of men. The one that learns by reading. The few who learn by observation. The rest of them have to pee on the electric fence for themselves.”
Will Rogers

I meant to do this earlier, but I got a little sidetracked on a completely different issue. Now that that little adventure is behind us, we can circle back to my recent changes to the Dynamic Service Portal Breadcrumbs and test everything out to see if it actually works. For that, I need to build a simple Portal Page, drop the breadcrumbs widget up at the top of the page, and then add a new widget that can listen for the messages coming out of the breadcrumbs widget.

So, let’s start with the new widget. This is just for testing, so we don’t need to get too fancy. Just a couple of test buttons, one to call for the return path and the other a back button that will use the return path. Here is some HTML that should work:

<div class="panel">
  <div class="row" style="text-align: center; padding: 25px;">
    <button class="btn btn-default" ng-click="testFunction();">Test Button</button>
  </div>
  <div class="row" style="text-align: center; padding: 25px;">
    <button class="btn btn-primary" ng-click="goBack();">Back Button</button>
  </div>
</div>

Each button has a function so will need to code those out in the Client Script to handle the clicks:

$scope.testFunction = function() {
	$rootScope.$broadcast('snhShareReturnPath');
};

$scope.goBack = function() {
	if (!c.data.returnPath) {
		$rootScope.$broadcast('snhShareReturnPath');
	}
	while (!c.data.returnPath) {
		pointlessCounter++;
	}
	window.location.href = c.data.returnPath;
};

The first one just broadcasts the message that we missed the return path, so please send it over again. The second one, which is an actual back button, looks to see if we have already obtained the return path, and if not, makes the same call to request it, and the drops into a loop until it shows up. I threw a counter in there just for something to do inside of the loop, but if I display that out, I can also see how long it takes to hear back from the request to rebroadcast the return path. Speaking of receiving the return path, we will need to add a little code to listen for that as well:

$rootScope.$on('snhBreadcrumbs', function(evt, info) {
	c.data.returnPath = info.returnPath;
	alert(pointlessCounter + '; ' + c.data.returnPath);
});

Wrapping all of that together with a little initialization code give us a complete Client Script for our new widget:

function TestWidget($scope, $rootScope) {
	var c = this;
	var pointlessCounter = 0;

	$scope.testFunction = function() {
		$rootScope.$broadcast('snhShareReturnPath');
	};

	$scope.goBack = function() {
		if (!c.data.returnPath) {
			$rootScope.$broadcast('snhShareReturnPath');
		}
		while (!c.data.returnPath) {
			pointlessCounter++;
		}
		window.location.href = c.data.returnPath;
	};

	$rootScope.$on('snhBreadcrumbs', function(evt, info) {
		c.data.returnPath = info.returnPath;
		alert(pointlessCounter + '; ' + c.data.returnPath);
	});
}

There is no server side script needed, so that completes our tester. Now we just have to throw it on a page and see what happens.

Test Page with the breadcrumbs and test widgets

Well, there is our test page containing the two widgets. There is no alert, though, which means that no one was listening when the breadcrumbs widget announced the return path, or something is broken. Let’s try the Test Button to see if we can get the breadcrumbs widget to announce it again.

Alert indicating the test widget has received the broadcast message

OK, that worked. Now, let’s try the back button.

The back button takes you back the previous screen in the breadcrumbs list

.. and that takes us back to the Incident that we were just looking at before we brought up the test page. Nice!

So far, so good. I tried a few more things, like clicking on the back button without first clicking on the test button and clicking on the test button multiple times. Everything seems to work the way in which I had envisioned it. I like it. As far as I can tell, it is good enough for me to put out another Update Set. Although I did get a little sidetracked on this one, it was a quick diversion and I did manage to circle back around and finish it up.

Update: There is a better (corrected) version here.

Dynamic Service Portal Breadcrumbs, Enhanced

“Build your own dreams, or someone else will hire you to build theirs.”
Farrah Gray

While I was playing around with my Configuration Item Icon Assignment widget, it occurred to me that it would be beneficial for my Dynamic Service Portal Breadcrumbs widget to somehow announce its presence to other widgets on the same page, and to provide them with the URL of previous page in case they wanted to set up some kind of Cancel or Done button that returned the user to the page from which they came. I even thought about putting such a button on the breadcrumbs widget itself, just under the row of breadcrumbs, but that seemed a little out of place, the more that I thought about it. So right now, I just want to let the other widgets know that they are sharing the page with the breadcrumbs widget and provide them with the return path.

The easiest way to do that is through some kind of root scope broadcast message, but that does present a little bit of a wrinkle, as you don’t really know which widgets have loaded first, and you could be broadcasting to no one if you are first to arrive on the scene. To solve that problem, you can listen for a different root scope message from other widgets that basically asks, “Please say that again now that I am ready to hear it.” From the listener’s perspective then, when you are ready to utilize the return path, if you haven’t yet received it, you can request it, and then go from there.

But first things first. Before we can announce the return path, we need to know what it is. Using the length of the breadcrumbs array as an index takes you past the end of the array, but subtracting 1 from that value gets you to the last entry, which happens to be the current page. We want the page before that, so we have to subtract 2 from the length. The danger there, of course, is that you might have an empty array or an array with a single element, and subtracting 2 would again put your index outside of the range of the array. So we have to start out with a default value, and then check the length of the array before we proceed any further. My code to do that looks like this:

c.returnPath = '?';
if (c.breadcrumbs.length > 1) {
	c.returnPath = c.breadcrumbs[c.breadcrumbs.length - 2].url;
}

That establishes the return path. Now we have to announce it. I created a function for that, since we will be doing this from multiple places (on load, and then again on request). Here is my function:

function shareReturnPath() {
	$rootScope.$broadcast('snhBreadcrumbs', {
		returnPath: c.returnPath
	});
}

Now that we have created the function, we need to call it as soon as we establish the return path, and then we need to set up a listener for later requests that will call it again on demand. For that, we just need a little more code wedged in between the two blocks above:

shareReturnPath();
$rootScope.$on('snhShareReturnPath', function() {
	shareReturnPath();
});

Well, that wasn’t so bad. Combined with what we had before, the entire Client Controller now looks like this:

function snhBreadcrumbs($scope, $rootScope, $location, spUtil) {
	var c = this;
	c.expanded = !spUtil.isMobile();
	c.expand = function() {
		c.expanded = true;
	};
	c.breadcrumbs = [];
	var thisTitle = c.data.page || document.title;
	var portalSuffix = ' - ' + $rootScope.portal.title.trim();
	var cutoff = thisTitle.indexOf(portalSuffix);
	if (cutoff != -1) {
		thisTitle = thisTitle.substring(0, cutoff);
	}
	var thisPage = {url: $location.url(), id: $location.search()['id'], label: thisTitle};
	
	if (thisPage.id && thisPage.id != $rootScope.portal.homepage_dv) {
		var pageFound = false;
		for (var i=0;i<c.data.breadcrumbs.length && !pageFound; i++) {
			if (c.data.breadcrumbs[i].id == thisPage.id) {
				c.breadcrumbs.push(thisPage);
				pageFound = true;
			} else {
				c.breadcrumbs.push(c.data.breadcrumbs[i]);
			}
		}
		if (!pageFound) {
			c.breadcrumbs.push(thisPage);
		}
	}
	c.data.breadcrumbs = c.breadcrumbs;
	c.server.update();
	c.returnPath = '?';
	if (c.breadcrumbs.length > 1) {
		c.returnPath = c.breadcrumbs[c.breadcrumbs.length - 2].url;
	}
	shareReturnPath();
	$rootScope.$on('snhShareReturnPath', function() {
		shareReturnPath();
	});

	function shareReturnPath() {
		$rootScope.$broadcast('snhBreadcrumbs', {
			returnPath: c.returnPath
		});
	}
}

Yes, we still have to test it, just to make sure that it all works in the way in which we intended, but it should work and it didn’t take all that much to provide this new functionality. In fact, it will probably be more work to test it than it was to create it. I’ll have to find or build another widget to share the page with this one, then create a page to put them both on, and then bring up the page and try things out. Now that I think about it, that seems like a decent amount of effort, so I think I will just save all of that until next time!

Configuration Item Icon Assignment Widget, Part III

When to use iterative development? You should use iterative development only on projects that you want to succeed.”
Martin Fowler

I probably could have wrapped all of this up last time, but I had not really given a whole lot of thought to a couple of critical decisions related to the last two items that needed to be coded out, the Save button and the Cancel button. For the Cancel button, the main question is where are you going to land once you leave the page. For the Save button, which is the bigger question, I need to figure out where I am going to store the information now that I have collected it on the screen. Since that’s a more complicated deliberation, let’s focus on the Cancel button first.

One thing that I wanted to do with the Cancel button was to pop up a confirmation that you really did want to bail, but only if you had made any changes that you would be losing by leaving the page. I wasn’t keeping track of whether or not you had made any changes, though, so I needed to fix that first. I created a variable called dataAltered and initialized it to false, and then I set it to true if you added or removed a row or selected an icon. With that in place, I built the function to handle the Cancel button clicks.

$scope.cancel = function() {
	if (dataAltered) {
		spModal.confirm('Discard your recent changes?').then(function(confirmed) {
			if (confirmed) {
				goBack();
			}
		});
	} else {
		goBack();
	}
};

Those of you paying close attention will notice that I still did not address the question of where to go after clicking the Cancel button; I just called a nonexistent function called goBack. That’s basically my way of putting things off until some future time when I am forced to deal with it, and usually I will create the function with a single line of code that pops an alert or write a log record to the let me know that I’ve made it that far. That let’s me test what I have done without having to deal with what I haven’t done. In this particular case, though, that was a short delay, because now it’s time to put real code in the goBack function, which means that it’s time to decide where people are going to land when they hit the Cancel button.

One of my first thoughts was that if this widget was sharing a screen with my Dynamic Service Portal Breadcrumbs widget, I could just go back one step in the chain to the previous screen. In order for that to work, I would need to know if I was sharing the page with that widget, which is not really possible at the moment. Ideally, that widget should announce its presence with some kind rootScope broadcast message that says something like, “Hey, I am here, and oh by the way, here is the URL for the previous page if you want to go back to it.” That actually sounds like a good idea, but it also sounds like a project for another day. I was planning on adding this widget to my existing Tools menu in the main UI, so I didn’t really see a lot of value in investing a lot of effort right now on getting it to work on the portal with breadcrumbs.

Ultimately, I decided to just go to the home page of the main UI, unless of course, we really were running in the portal, and in that case, I would just return to the home page of the portal. Since the main UI pages run inside of an iFrame, it seemed relatively simple enough to detect the difference by looking at the parent window. The final code turned out to be just this:

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

That takes care of the easy part. Now it is finally time to make a decision on where to store this information. The simplest thing to do seemed to be to just add another column to the sys_db_object table for the name of the icon. That ‘s a fairly foundational table to the whole ServiceNow platform, though, and I really do not like to mess with those if I can avoid it. A preferable alternative would be to create a simple m2m table that had a column for the CI class and another column for the icon name. That’s much better, but there is still a certain amount of work involved in creating the table, saving the data to the table, and then reading the data back in from the table whenever you wanted to use it. What I finally decided to do was to just keep the icon map hard-coded in my Script Include, and then just update the script column of the Script Include whenever the Save button is clicked. Look, Ma — no tables!

My plan was to grab the script, substring off the parts of the script before and after the property list, rebuild the property list from the data on the screen, and then rebuild the script from the before and after pieces and the new property list. There is probably some regex wizardry that would do all of that with a single line of code, but most of that is all voodoo magic as far as I can tell, and I like to build things that can be comprehended by a little bit larger audience. In the end, my approach seemed to have an awful lot of code and variables involved, but it does work, so there you go.

function save() {
	var scriptGR = new GlideRecord('sys_script_include');
	if (scriptGR.get('7b6ebf052f8e18104425fcecf699b6f6')) {
		var original = scriptGR.getValue('script');
		var i = original.indexOf('map: {');
		if (i != -1) {
			var preamble = original.substring(0, i);
			var theRest = original.substring(i);
			i = theRest.indexOf('\n\t},');
			if (i != -1) {
				var postamble = theRest.substring(i);
				var separator = '';
				var newScript = preamble + 'map: {';
				for (i=0; i<data.itemArray.length; i++) {
					newScript += separator + '\n\t\t' + data.itemArray[i].id + ": '" + data.itemArray[i].icon + "'";
					separator = ',';
				}
				newScript += postamble;
				scriptGR.setValue('script', newScript);
				scriptGR.update();
				gs.addInfoMessage('The Configuration Item icon assignments have been saved');
			} else {
				gs.addErrorMessage('The Script Include to update has been corrupted.');
			}
		} else {
			gs.addErrorMessage('The Script Include to update has been corrupted.');
		}
	} else {
		gs.addErrorMessage('The Script Include to update was not found.');
	}
}

That’s a server side script, by the way, so we still need a client side script to send things over to the server. That one is pretty simple, though, and looks like this:

$scope.save = function() {
	c.data.action = 'save';
	c.server.update();
};

In fact, the whole client side script, including the code that we just added to detect changes and handle the two buttons now looks like this in its final form:

function CIIconAssignment($scope, spModal) {
	var c = this;
	var dataAltered = false;

	$scope.addSelected = function() {
		c.server.update();
		$('#snrp').select2("val","");
		dataAltered = true;
	};

	$scope.removeItem = function(inx) {
		spModal.confirm('Remove ' + c.data.itemArray[inx].id + ' from the list?').then(function(confirmed) {
			if (confirmed) {
				c.data.itemArray.splice(inx, 1);
				dataAltered = true;
			}
		});
	};

	$scope.selectIcon = function(inx) {
		spModal.open({
			title: 'Select Icon',
			widget: 'icon-picker',
			buttons: [
				{label: '${Cancel}', cancel: true}
			],
			size: 'sm',
		}).then(function(response) {
			c.data.itemArray[inx].icon = response.selected;
			dataAltered = true;
		});
	};

	$scope.save = function() {
		c.data.action = 'save';
		c.server.update();
	};

	$scope.cancel = function() {
		if (dataAltered) {
			spModal.confirm('Discard your recent changes?').then(function(confirmed) {
				if (confirmed) {
					goBack();
				}
			});
		} else {
			goBack();
		}
	};

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

The last thing that I did was grab one of the existing sidebar menu options under my Tools section and use it as model for a new menu option to launch this page. After that, I used the new option to bring up the page and test everything out. Everything seems to work, and it came out looking pretty good, all things considered.

Configuration Item Icon Assignment widget and related menu option

One thing that I did realize during my testing is that there is nothing to stop you from deleting the root cmdb_ci entry, which serves as my default. I don’t really want you doing that, so I added an ng-hide to the delete icon that will remove that option when the item.id is cmdb_ci. You can see in the image above that there is no delete option for that row. That’s much better.

After playing around and testing, I ended up saving my Script Include several times over. Right now, it looks like this:

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

	getIcon: function(ciClass) {
		var icon = this.map[ciClass];

		if (!icon) {
			ciClass = this.getParentClass(ciClass);
			if (ciClass) {
				icon = this.getIcon(ciClass);
			}
		}

		return icon;
	},

	getParentClass: function(ciClass) {
		var parentClass = '';

		var tableGR = new GlideRecord('sys_db_object');
		if (tableGR.get('name', ciClass)) {
			parentClass = tableGR.super_class.name;
		}

		return parentClass;
	},

	map: {
		cmdb_ci: 'configuration',
		cmdb_ci_aix_server: 'connect-viewdocument',
		cmdb_ci_computer: 'hardware',
		cmdb_ci_config_file: 'document',
		cmdb_ci_database: 'database',
		cmdb_ci_hardware: 'keyboard',
		cmdb_ci_linux_server: 'sp-wishlist-sm',
		cmdb_ci_server: 'server',
		cmdb_ci_spkg: 'software',
		cmdb_ci_unix_server: 'text-underlined',
		cmdb_ci_win_server: 'vtb-freeform'
	},

	type: 'ConfigurationItemIconMap'
};

Of course, that will change the next time that I use the tool, but I like the fact that I did not end up creating any extra columns or tables to pull this off. For those of you who like to play along at home, here is an Update Set containing all of the parts and pieces.

Dynamic Service Portal Breadcrumbs, Perfected

“Perfection is finally attained not when there is no longer anything to add but when there is no longer anything to take away, when a body has been stripped down to its nakedness.”
Antoine de Saint-Exupéry

Years ago, I was taught that there was three kinds of software maintenance, Corrective, Adaptive, and Perfective. There are those that will tell you that there is a fourth kind, called Preventive or Preventative depending on where and when you learned the English language, but in my mind, that doesn’t really apply to software. Preventative maintenance is done to physical assets like a vehicle or an elevator or a machine on the factory floor. The idea behind preventative maintenance is to lubricate or replace parts before they wear out to prevent a breakdown. Preventative maintenance is usually performed on a schedule based on the anticipated lifespan of the parts in question. Having your car serviced by the dealer periodically in accordance with the schedule laid out in the owner’s manual is a form of preventative maintenance. Software, though, is not constructed out of physical parts that wear out, so I can’t imagine what task that you could perform on a piece of software that you could consider preventive. But, I digress …

Today, we’re going to talk about some Perfective Maintenance done on my old friend, the Dynamic Service Portal Breadcrumbs widget. I didn’t want to change its function or features — that wouldn’t be maintenance at all; that would be an Enhancement — and I wasn’t trying to fix anything (Corrective) and I wasn’t trying to conform to a new browser or a new version of ServiceNow (Adaptive). I just wanted to make it better, which is the essence of Perfective Maintenance.

But how could it possibly be any better, you ask? While that is definitely a reasonable question, there is always room for improvement on just about everything. One thing that has always annoyed me was the presence of the portal title in breadcrumbs when just the page title would do. You can see an example of that on this image from an earlier installment on this component:

Breadcrumbs with Page Title/Portal Title as Label

I never liked that. It was redundant, and it made individual elements of the breadcrumbs much wider than they needed to be. So I added this code to search for that and remove it:

var thisTitle = c.data.page || document.title;
var portalSuffix = ' - ' + $rootScope.portal.title.trim();
var cutoff = thisTitle.indexOf(portalSuffix);
if (cutoff != -1) {
	thisTitle = thisTitle.substring(0, cutoff);
}
var thisPage = {url: $location.url(), id: $location.search()['id'], label: thisTitle};

That should work for as long as ServiceNow tacks the portal title on to the end of the page title using a dash surrounded by single spaces as a separator. If that convention ever changes, then this will need to be revisited and adjusted accordingly.

One other thing that always disturbed me was that the breadcrumb trail lasted forever, and if you were off of the site for a few days and then came back, your breadcrumbs from your last session would still be active. It seemed to me that, once you have been gone for some time, your breadcrumbs should start fresh back at the home page again. To make that happen, I added one more User Preference for the session ID, and only used the breadcrumb trail if it came from the same session. That code lives on the server side, and looks like this:

data.breadcrumbs = [];
var snhbc = gs.getPreference('snhbc');
var snhses = gs.getPreference('snhses');
if (snhbc && snhses && snhses == thisSession) {
	data.breadcrumbs = JSON.parse(snhbc);
}

One other thing I did was to look at the generic display value for the current record before searching for specific fields. Many tables already have a display field selected as part of the table definition, so using the getDisplayValue() method without passing any arguments can save all of that hunting around for the right field to use. If there is no defined display field, though, it will return the creation date with a label of ‘Created’, which we don’t want, so we have to check for that as well. That new code now looks like this:

data.page = rec.getDisplayValue();
if (data.page.indexOf('Created') == 0) {
	data.page = null;
}
if (!data.page) {
	data.page = rec.getDisplayValue('number');
}
if (!data.page) {
	data.page = rec.getDisplayValue('name');
}
if (!data.page) {
	data.page = rec.getDisplayValue('short_description');
}
if (!data.page) {
	data.page = rec.getLabel();
}

So, just a few little tweaks here and there to spruce things up a bit. No features added, no features taken away, and no defects repaired. Just a little tidying up to make it even better than it was before. For those of you who like to play along at home, here is the most recent Update Set.

Update: There is an even better version, which you can find here.