Configurable Data Table Widget Content Selector, Revisited

“Learning from mistakes and constantly improving products is a key in all successful companies.”
Bill Gates

When I first conceived of my Configurable Data Table Widget Content Selector, my main focus was on creating a process that would read the JSON configuration file and turn those configuration rules into a functioning widget in accordance with the specifications. That was an interesting challenge that I had a quite a bit of fun with, but I started out by hard-coding the configuration object at the beginning of the widget and I never went back and set things up so that you could reuse the widget with a different configuration. Obviously, that’s not very friendly; now I need to go back in and set things right. The configuration object that you want to use should be an external parameter that gets passed into the widget via some external source such as a URL parameter or widget option. Let’s see if we can’t fix that right now.

I think that the first thing that I want to do is to create a base class for the configuration object script. That will do two things: 1) provide a common foundation of code for all of the configuration objects that you would like to build, and 2) provide a way to identify all of the qualifying scripts as all scripts that extend this particular base class. We’ll call it the ContentSelectorConfig:

var ContentSelectorConfig = Class.create();
ContentSelectorConfig.prototype = {

	initialize: function() {
	},

	getConfig: function(sp) {
		return {
			perspective: this.perspective,
			state: this.state,
			table: this.table
		};
	},

	type: 'ContentSelectorConfig'
};

With our base class established, I can now hack up my earlier configuration object and make it an extension of this new base class:

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

	perspective: [...],

	state: [...],

	table: {...},

	type: 'MyTaskConfig'
});

For now, I am just going to reuse the list of perspectives, states, and tables that I was using before and focus on the mechanics of making this configuration an external parameter rather than a hard-coded reference. Rather than make the changes to my original My Data widget, though, I decided to clone the widget, give it a new name, and leave the original widget as is. I called my new widget Content Selector, and it started out life as an exact copy of the My Data widget before I started to hack it up.

The first thing that I did was to add a new option to the Option Schema so that we could pass in the name of the ContentSelectorConfig that we want to use. We already had an existing option to display the widget content in a single row rather than a stacked block, so this was just a matter of adding a second option to the existing array of options.

[{
  "hint":"Mandatory configuration script that is an extension of ContentSelectorConfig",
  "name":"configuration_script",
  "section":"Behavior",
  "label":"Configuration Script",
  "type":"string"
},{
  "hint":"If selected, will display the widget content in a single row rather than a stacked block",
  "name":"display_inline",
  "default_value":"false",
  "section":"Presentation",
  "label":"Display Inline",
  "type":"boolean"
}]

Now that our new option has been defined, it’s time to rewrite the code that pulled in the hard-coded configuration object. Here is the original version of the first few lines of code in the server side script:

var mdc = new MyDataConfig();
data.config = mdc.getConfig($sp);
data.config.authorizedPerspective = getAuthorizedPerspectives();
establsihDefaults();
data.user = data.user || {sys_id: gs.getUserID(), name: gs.getUserName()};
data.inline = false;
if (options && options.display_inline == 'true') {
	data.inline = true;
}

We are still going to want to establish the data.config object, but we are going to want to do it using an instance of the class named in our new Option. Last year, when I was working on my Static Monthly Calendar, I needed to turn a class name into an object of that class, and I built a little tool for that, which I called the Instantiator. We can use that same tool here to turn the name of our configuration script into an instance of that class that we can use to pull in the configuration. Here is the restructured code to start out our updated widget:

data.config = {};
data.inline = false;
data.user = data.user || {sys_id: gs.getUserID(), name: gs.getUserName()};
if (options) {
	if (options.configuration_script) {
		var instantiator = new Instantiator();
		instantiator.setRoot(this);
		var configurator = instantiator.getInstance(options.configuration_script);
		data.config = configurator.getConfig($sp);
		data.config.authorizedPerspective = getAuthorizedPerspectives();
		establsihDefaults();
	}
	if (options.display_inline == 'true') {
		data.inline = true;
	}
}

Now we just need to throw it on a page with a Data Table widget, configure the options, and give it a spin. After dragging all of the widgets onto the page in the Page Designer, clicking on the pencil icon in the upper right-hand corner of our update widget will bring up the Options dialog where we can specify our configuration script.

Widget Options dialog

Once the widget Options have been specified, all we need to do is to pull up the page and see if things are still functioning as they should, which appears to be the case.

Testing the completed modifications

Well, that’s about all there is to that. Basically, it does exactly what it did before, but you can now specify the configuration script using the Widget Options instead of having it hard-coded in the script as it was in the original version. Here’s an Update Set with the modifications if you’d like to play around with it on your own.

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.

sn-record-picker Helper, Corrected

“We are what we repeatedly do. Excellence, then, is not an act, but habit.”
Aristotle

The other day I was using my sn-record-picker Helper to create a picker that allowed multiple selections and I discovered that there were a couple of undetected errors in there that needed to be cleaned up. I rarely have an occasion to use the multiple=”true” option, so I never noticed the issues before. The first one was relatively simple: there was an extra trailing quote in the generated code for the multiple attribute. That was easy enough to fix. The other one was a little more complicated. The live example of the configured picker was never set up to handle multiple selections. That seemed like it would be a relatively easy fix, but it turned out to be a little more complicated than I realized.

My first thought was to just add the multiple attribute to the tag, and set the value to the value of the checkbox on the form, thinking that it would resolve to true or false and take care of the problem.

<snh-form-field
  snh-model="c.data.liveExample"
  snh-name="liveExample"
  snh-label="Live Example"
  snh-type="reference"
  snh-change="optionSelected()"
  placeholder="{{c.data.placeholder}}"
  table="c.data.table.value"
  display-field="c.data.displayField.value"
  display-fields="c.data.displayFields.value"
  value-field="c.data.valueField.value"
  search-fields="c.data.searchFields.value"
  default-query="c.data.filter"
  multiple="c.data.multiple">

Unfortunately, that didn’t work. It didn’t really do anything bad, it just did not render out as a multiple selection picker, even when I checked the box. I thought maybe that it needed to be interpreted/resolved, so I surrounded the variable with double curly braces.

  multiple="{{c.data.multiple}}">

Things really went South at that point. The whole thing crashed with the following error:

invalid key at column 2 of the expression [{{c.data.multiple}}] starting at [{c.data.multiple}}].

So, I tried a number of other, different things, none of which seemed to do the trick. Apparently, you have to hard-code the value of that attribute to true or it just won’t work. So much for making it dynamic. So, in the end, I had to create two versions of the element, one for single and another nearly identical one for multiple, and then show or hide them based on the value of the c.data.multiple variable.

<snh-form-field
  ng-hide="c.data.multiple"
  snh-model="c.data.liveExample"
  snh-name="liveExample"
  snh-label="Live Example"
  snh-type="reference"
  snh-change="optionSelected()"
  placeholder="{{c.data.placeholder}}"
  table="c.data.table.value"
  display-field="c.data.displayField.value"
  display-fields="c.data.displayFields.value"
  value-field="c.data.valueField.value"
  search-fields="c.data.searchFields.value"
  default-query="c.data.filter">
</snh-form-field>
<snh-form-field
  ng-show="c.data.multiple"
  snh-model="c.data.liveExample"
  snh-name="liveExample"
  snh-label="Live Example"
  snh-type="reference"
  table="c.data.table.value"
  display-field="c.data.displayField.value"
  display-fields="c.data.displayFields.value"
  value-field="c.data.valueField.value"
  search-fields="c.data.searchFields.value"
  default-query="c.data.filter"
  multiple="true">
</snh-form-field>

Not the most elegant solution, but it does work, so there’s that. One thing that did not work on the multiple version was the modal pop-up on change. That works pretty slick on the single selection version, but on the multiple, the change event never fires. I played around with that for a while looking for a solution, but I finally gave up and just removed that attribute from the multiple version, since it didn’t actually do anything. On the multiple version, everything that you have selected is already displayed right there in front of you, so I figured that we weren’t losing all that much by my not finding a ready solution to the problem.

So that’s it: two little fixes. It’s not all that much, but it does correct a couple of annoying little problems, so here’s a fresh Update Set with the corrections in place.

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

Flow Designer Array Iterator

“It is by logic that we prove, but by intuition that we discover.”
Henri Poincaré

One of the reasons that I built my little Flow Designer Scratchpad was to keep track of an index while I looped through items in an Array. After doing a few of those, I decided it would be even nicer if I had some kind of iterator Action that would do the work of incrementing the index and returning the Array element at the current index. I already had the scratchpad to store all of the data necessary to support an iterator, so it seemed as if I could write some kind of Action that would take a Scratchpad ID, an Iterator ID, and an Array as input and use that information to set up the ability to iterate through the Array provided. As usual, I decided to put the bulk of the code in a Script Include to keep the actual code in the Action down to the absolute minimum. Here is the SNHStringArrayUtils that I came up with:

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

	createIterator: function(scratchpadId, interatorId, stringArray) {
		var response = {};

		var snhspu = new SNHScratchpadUtils();
		response = snhspu.setScratchpadProperty(scratchpadId, interatorId, interatorId);
		if (response.success) {
			if (Array.isArray(stringArray)) {
				var iterator = {};
				iterator.index = 0;
				iterator.array = stringArray;
				response = snhspu.setScratchpadProperty(scratchpadId, interatorId, JSON.stringify(iterator));
				if (response.success) {
					response.message = 'Array Interator ' + interatorId + ' successfully created';
				}
			} else {
				response.success = false;
				response.message = 'String Array parameter is missing or invalid';
			}
		}

		return response;
	},

	iteratorNext: function(scratchpadId, interatorId) {
		var response = {};

		var snhspu = new SNHScratchpadUtils();
		response = snhspu.getScratchpadProperty(scratchpadId, interatorId);
		if (response.success) {
			var iterator = {};
			try {
				iterator = JSON.parse(response.property_value);
			} catch (e) {
				response.success = false;
				response.message = 'Unable to parse JSON string containing iterator details';
			}
			if (response.success) {
				if (iterator.index >= 0 && iterator.index < iterator.array.length) {
					response.current_value = iterator.array[iterator.index];
					response.current_index = iterator.index;
					iterator.index++;
					response.has_next = (iterator.index < iterator.array.length);
					response.message = 'The current value at index ' + response.current_index + ' is ' + response.current_value;
					snhspu.setScratchpadProperty(scratchpadId, interatorId, JSON.stringify(iterator));
				} else {
					response.success = false;
					response.message = 'Current index value out of range';
				}
			}
		}

		return response;
	},

	type: 'SNHStringArrayUtils'
};

Basically, there are two methods, one for each of the two Flow Designer Actions that I intend to build. The first one is createIterator, which is used to initialize a new iterator, and the second is iteratorNext, which will support the Action that you will invoke inside of your loop to get the next item in the Array. Both utilize an existing scratchpad, so you will need to create that prior to invoking these Actions, and both require an Iterator ID, which is just a unique key to be used in storing the iterator data in the scratchpad. The createIterator action would be called once outside of the loop, and then the iteratorNext function would be called inside of the loop, usually right at the top to pull out the next value in the array.

The iterator itself is just a two-property object containing the array and the current value of the index. This is converted to a JSON string and stuffed into the scratchpad using the passed Iterator ID as the key. When creating the iterator, we set the index value to zero, and in the next Action, after using the index to get the current element, we increment it and update the scratchpad. Now that we have the basic code to support the two Actions, we need to go into the Flow Designer and create the Actions.

The Create Array Iterator Action seems like the logical place to start. That will will need three Inputs defined.

The Create Array Iterator Action Inputs

… and it will need two Outputs defined, a success indicator and an optional error message detailing any failure to perform its intended function.

The Create Array Iterator Action Outputs

In between the Inputs and Outputs will be a simple Script step, where we will produce the Outputs by passing the Inputs to our associated Script Include function.

var snhsau = new SNHStringArrayUtils();
var result = snhsau.createIterator(inputs.scratchpad_id, inputs.iterator_id, inputs.string_array);
for (var key in result) {
	outputs[key] = result[key];
}

That’s pretty much all there is to that. We can test it using the Test button up at the top of the Action Editor, but first we will need a Scratchpad. We can take care of that real quick by hitting the Test button on the Create Scratchpad Action and then grabbing the Scratchpad ID from the Outputs. With our Scratchpad ID in hand, we can now test our Create Array Iterator Action.

Testing the Create Array Iterator Action

So far so good. now we just need to do the same thing for the Array Iterator Next Action, and we’ll be all set.

The Array Iterator Next Action

This is pretty much a rinse and repeat kind of thing, with fewer Inputs, but more Outputs. When it comes time to test, we can use the Scratchpad ID and Iterator ID from our last test, and then run it through a few times to see the different results at different stages of the process. Rather that go through all of that here, I will just bundle everything up into an Update Set, and you can pull it down and play with it on your own.

Note: With the introduction of Flow Variables, this component is no longer necessary.

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.

Flow Designer Scratchpad

“Necessity is the mother of invention.”
English-language proverb

I really should be working on testing out the latest enhancements to my Dynamic Service Portal Breadcrumbs right now, but I ran into this other issue recently, and I really want to see if I can make this work. I hate it when people start something and then move on to other things without ever finishing up what they started, so I definitely want to circle back and wrap that one up; however, today is not that day.

Today I want to talk about the Flow Designer. I have been striving to convert any of my old legacy Workflows over to the newer Flow Designer tool whenever the opportunity arises. The other day I was doing just that with a Workflow that made extensive use of the Workflow Scratchpad feature. When I went to look for the equivalent feature in the Flow Designer, I couldn’t find anything. I thought maybe that it wasn’t needed for some reason, so I tried several workarounds, but nothing worked. Nothing that I tried would preserve and/or modify data between or across Actions or Subflows. After quite a number of failed attempts to find something that would do the job, I eventually came to realize that if I wanted some kind of Scratchpad capability in the Flow Designer, I was going to have to build it myself.

My first thought was that all that I would need would be simple setProperty and getProperty functions, but then I realized that I would first need to establish the scratchpad, and once established, I would want to be able to get rid of it as well, so that turned into four relatively simple functions, which is still not too bad. When I say functions, what I really mean are Flow Designer Actions, but since I will be calling some function in a common Script Include built for this purpose, I still think of them as functions. Here is the function to create a scratchpad, which is just basically a record on a table that I created for this purpose:

createScratchpad: function() {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	spGR.initialize();
	spGR.setValue('u_scratchpad', '{}');
	if (spGR.insert()) {
		response.success = true;
		response.scratchpad_id = spGR.getUniqueValue();
		response.message = 'Scratchpad record successfully created';
	} else {
		response.success = false;
		response.message = 'Unable to create scratchpad record';
	}

	return response;
},

The scratchpad itself is just a JSON string stored in the only column added to the table, u_scratchpad. We initialize that to an empty object and save the record and that’s about all there is to that. To get rid of it, we will need to have the sys_id of the record, but there is not much code behind that process, either:

deleteScratchpad: function(spId) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		if (spGR.deleteRecord()) {
			response.success = true;
			response.message = 'Scratchpad record successfully deleted';
		} else {
			response.success = false;
			response.message = 'Unable to delete scratchpad record';
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

That takes care of building up and tearing down the scratchpad object. Now, to use it, we will need those setProperty and getProperty functions that we were talking about earlier. This one will let you set the value of a property on the scratchpad:

setScratchpadProperty: function(spId, propertyName, propertyValue) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		var jsonString = spGR.getValue('u_scratchpad');
		var jsonObject = {};
		try {
			jsonObject = JSON.parse(jsonString);
		} catch(e) {
			response.warning = 'Unable to parse JSON string: ' + e;
		}
		jsonObject[propertyName] = propertyValue;
		jsonString = JSON.stringify(jsonObject, null, '\t');
		spGR.setValue('u_scratchpad', jsonString);
		if (spGR.update()) {
			response.success = true;
			response.message = 'Scratchpad property "' + propertyName + '" set to "' + propertyValue + '"';
		} else {
			response.success = false;
			response.message = 'Unable to update scratchpad record';
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

… and this one lets you retrieve the value of a property on the scratchpad:

getScratchpadProperty: function(spId, propertyName) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		var jsonString = spGR.getValue('u_scratchpad');
		try {
			var jsonObject = JSON.parse(jsonString);
			var propertyValue = jsonObject[propertyName];
			if (propertyValue > '') {
				response.success = true;
				response.property_value = propertyValue;
				response.message = 'Returning value "' + propertyValue + '" for scratchpad property "' + propertyName + '"';
			} else {
				response.success = false;
				response.message = 'Scratchpad property "' + propertyName + '" has no value';
			}
		} catch(e) {
			response.success = false;
			response.message = 'Unable to parse JSON string: ' + e;
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

That’s it for the core functions needed to make this work. Putting it all together, the entire Script Include looks like this:

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

	createScratchpad: function() {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		spGR.initialize();
		spGR.setValue('u_scratchpad', '{}');
		if (spGR.insert()) {
			response.success = true;
			response.scratchpad_id = spGR.getUniqueValue();
			response.message = 'Scratchpad record successfully created';
		} else {
			response.success = false;
			response.message = 'Unable to create scratchpad record';
		}

		return response;
	},

	setScratchpadProperty: function(spId, propertyName, propertyValue) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			var jsonString = spGR.getValue('u_scratchpad');
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch(e) {
				response.warning = 'Unable to parse JSON string: ' + e;
			}
			jsonObject[propertyName] = propertyValue;
			jsonString = JSON.stringify(jsonObject, null, '\t');
			spGR.setValue('u_scratchpad', jsonString);
			if (spGR.update()) {
				response.success = true;
				response.message = 'Scratchpad property "' + propertyName + '" set to "' + propertyValue + '"';
			} else {
				response.success = false;
				response.message = 'Unable to update scratchpad record';
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	getScratchpadProperty: function(spId, propertyName) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			var jsonString = spGR.getValue('u_scratchpad');
			try {
				var jsonObject = JSON.parse(jsonString);
				var propertyValue = jsonObject[propertyName];
				if (propertyValue > '') {
					response.success = true;
					response.property_value = propertyValue;
					response.message = 'Returning value "' + propertyValue + '" for scratchpad property "' + propertyName + '"';
				} else {
					response.success = false;
					response.message = 'Scratchpad property "' + propertyName + '" has no value';
				}
			} catch(e) {
				response.success = false;
				response.message = 'Unable to parse JSON string: ' + e;
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	deleteScratchpad: function(spId) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			if (spGR.deleteRecord()) {
				response.success = true;
				response.message = 'Scratchpad record successfully deleted';
			} else {
				response.success = false;
				response.message = 'Unable to delete scratchpad record';
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	type: 'SNHScratchpadUtils'
};

Now that we have all of the functions, we need to turn those into Flow Designer Actions. Before we do that, though, let’s create a Category for them so that we can group them and they will be easy to find. We do that by adding a row to the Action Category table, sys_hub_category. With that out of the way, we can create our first Action, which will invoke the createScratchpad function in our Script Include.

Create Scratchpad Action

The entire Action is just a Script step that leverages our Script Include and passes the results on to the Action Outputs. The small script to make that happen is just a few short lines of code:

var snhspu = new SNHScratchpadUtils();
var result = snhspu.createScratchpad();
for (var key in result) {
	outputs[key] = result[key];
}

Now we just need to repeat that process 3 more times to create Flow Designer Actions from our three other Script Include functions and we’re all set. To test things out, there is a little Test button right at the top of the Flow Designer page, and for the Create Scratchpad Action, there isn’t even any input to set up, so you can just click that button and go. Once you test out the Create Scratchpad Action, you can snag the Scratchpad ID out of the Action Outputs and then use that as an input to test all of the others.

Well, that wasn’t so bad: one Script Include, four functions, one Action Category, and four Actions. I threw this together rather quickly, but here is the Update Set. If you run into any issues with that, or if you can think of any way to make it better, please let me know in the comments. Of if you know of a built-in function that eliminates the need for this, that would be even better!

Note: With the introduction of Flow Variables, this component is no longer necessary.

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.

Configuration Item Icon Assignment Widget, Part II

Walking on water and developing software from a specification are easy if both are frozen.”
Edward V. Berard

It wasn’t entirely true that my empty shell CI Icon Assignment widget was completely devoid of all code underneath. I did have to toss in the function that launches the Retina Icon Picker in order to show that in action, but I was able to steal most of that code from the little test page that I had built to try that guy out.

$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;
	}});
};

In my original test page, I had a second response function to handle the Cancel button, but for our purposes here, that’s not really necessary. If you hit the Cancel button on the pop-up on this widget, it will just close and nothing else will happen, which is perfectly fine. The other alteration that I had to make was to include an index argument so that when you did make a selection, it was applied to the appropriate line. Other than that, it’s pretty much what we were working with earlier.

But the icon picker is not the only element on the screen that needs a little code behind it. We’ll need to do something with the Configuration Item picker as well. Selecting a CI from the picker should result in a new row added to the table for the selected CI, and then the picker should be cleared in preparation for another selection to be made. Since we have to go look up the label for our second column, that sounds like a server side operation, so our client side code will be pretty simple:

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

The first line just kicks the process over to the server side, and then then second line clears out the value of the sn-record-picker so that it is ready for another selection. Over on the server side, we do the work to add the new CI type to the list, as long as it isn’t already there.

if (input) {
	data.itemArray = input.itemArray;
	if (input.classToAdd && input.classToAdd.value) {
		addClassToList(input.classToAdd.value);
	}
}

function addClassToList(key) {
	var foundIt = false;
	for (var i=0; !foundIt && i<data.itemArray.length; i++) {
		if (data.itemArray[i].id == key) {
			foundIt = true;
		}
	}
	if (!foundIt) {
		var thisItem = {};
		thisItem.id = key;
		thisItem.name = getItemName(key);
		data.itemArray.push(thisItem);
	}
	input.classToAdd = {};
}

function getItemName(key) {
	var ciGR = new GlideRecord(key);
	return ciGR.getLabel();
}

That takes care of adding an item to the list. Now we have to add some code to handle removing an item from the list. We put a Delete icon at the end of each line, so we just need to build the code that will run when someone clicks on that icon. Since it is a delete, we will want to pop up a quick confirm dialog before we actually remove the item from the list, just to make sure that they did that on purpose. All of that can be handled on the client side, and fairly simply.

$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);
		}
	});
};

To test all of that out, I added a few items to the list, selected an icon for each, and then tried to remove a few. During my testing, I realized that adding enough items to the list scrolls the icon selector off of the page and out of sight, which I didn’t really like, so I did a little rearranging and put the selector on top of the list instead of underneath. With that change, the modal delete confirmation now looks like this:

Delete confirmation pop-up on reconfigured screen layout

So now we have all of the code in place to select a CI, to select an icon for the CI, and to remove a CI from the list. Now all we have left to do is to put some code under the two buttons at the bottom of the screen. Of course, coding the Save button would be much easier if I knew where I was going store this information once I am through building the list, but I haven’t really given much thought to that just yet, since we hadn’t quite gotten to that part until just this moment. Now that writing the code for the buttons is the only thing left to do, it’s time to figure that out. But that could potentially be an entire installment in and of itself, so I think we’ll stop here and save that exercise for another day.

Configuration Item Icon Assignment Widget

“You don’t have to see the whole staircase, just take the first step.”
Martin Luther King, Jr.

The main reason that I wanted to find a way to include an Icon in a pick list is because I wanted to assign different icons to various classes of Configuration Items to help visually distinguish the items based on their type. Now that I know that I can’t just add an icon to a single SELECT statement, I’m not exactly sure how I am going to that, but that’s not today’s issue. When all is said and done, I may not even be able to do what I am hoping to do, but I’m not working on that part right now. I try not to get too distracted by the things that I don’t understand or don’t know how I’m going to accomplish before it’s time. Since I can’t code everything all at once, I don’t need to solve all of the mysteries all at once, either. My theory is that I should be able to figure it out when the time comes, so there is no need to worry about it right now. To keep productive, and to maintain focus, I like to deal with things One Piece at a Time.

Today, I want to build a function that will return the appropriate icon name based on a passed configuration item class, which will give me something to call when it is time to go get the icon associated with a particular item. I want to return an icon name in all circumstances, so if the specific CI class is not mapped to an icon, then I want the function to check the parent class, and basically keep doing that until an icon name is found. If it makes it all the way to the top and there is still no icon, then there needs to be a default, because one way or another, I want to return an icon name no matter what. I will eventually stuff this function into a Script Include of some kind, but for now, I just want to code out the function. Here is what I came up with.

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;
},

OK, it turns out that it is actually two functions, but you get the idea. This code assumes that there is a map included that associates icon names to CI classes, and that the map is keyed by CI class and returns the icon name. We don’t have to have the fully populated map right now, but to test the code, we will at least need to stub it out with a minimum of one item. That should be fairly simple to do.

map: {
	cmdb_ci: 'configuration'
},

Since the cmdb_ci class is basically the root class of all Configuration Items, defining a value for that class essentially establishes the default. As you crawl up the parentage of any specific CI class, you will eventually find your way to the cmdb_ci class, so that should satisfy my requirement that there should always be a default response from the function call.

The stubbed-out map is a good start, but I want to build up my map using some kind of tool that will allow me to select a CI class and then use my new icon picker to select an appropriate icon for the class. Something that would look like this:

CI/Icon map maintenance tool

Once you added your CI class to the list, then you could click on the little magnifying glass icon to launch our icon picker to select your icon.

Using the Icon Picker to select an icon for a CI class

What you are seeing is just a screen mock up at this point, but putting the screen together is always a good place to start. Here is the HTML that I used to produce the screen image.

<div class="panel">
<div style="width: 100%; padding: 5px 50px;">
  <h2 style="width: 100%; text-align: center;">${Configuration Item Icon Assignment}</h2>
  <table class="table table-hover table-condensed">
    <thead>
      <tr>
        <th style="text-align: center;">Item</th>
        <th style="text-align: center;">Label</th>
        <th style="text-align: center;">Icon</th>
        <th style="text-align: center;">Icon Name</th>
        <th style="text-align: center;">Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in c.data.itemArray track by item.id | orderBy: 'id'" ng-hide="item.removed">
        <td data-th="Item"><input class="form-control" ng-model="item.id" readonly="readonly"/></td>
        <td data-th="Label"><input class="form-control" ng-model="item.name" readonly="readonly"/></td>
        <td data-th="Icon" style="text-align: center;"><span style="font-size: 25px;" class="icon icon-{{item.icon}}"></span></td>
        <td data-th="Icon Name">
          <span class="input-group" style="width: 100%;">
            <input class="form-control" ng-model="item.icon" readonly="readonly"/>
            <span class="input-group-btn" ng-click="selectIcon($index)" aria-hidden="false">
              <button class="btn-ref btn btn-default">
                <span class="icon icon-search" aria-hidden="true"></span>
                <span class="sr-only ng-binding">${Select an icon}</span>
              </button>
            </span>
          </span>
        </td>
        <td data-th="Delete" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="removeItem($index)" alt="Click here to remove this item from the list" title="Click here to remove this item from the list" style="cursor: pointer;"/></td>
      </tr>
    </tbody>
  </table>
  <p>To add another Configuration Item to the list, select an item from below:</p>
  <sn-record-picker
    id="snrp"
    field="data.classToAdd"
    ng-change="addSelected()"
    table="'sys_db_object'"
    default-query="'super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54'"
    display-field="'label'"
    display-fields="'name'"
    search-fields="'label'"
    value-field="'name'">
  </sn-record-picker>
  <br/>
  <p>To remove an item from the list, click on the Delete icon.</p>
</div>

<div style="width: 100%; padding: 5px 50px; text-align: center;">
  <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
   
  <button ng-click="cancel()" class="btn ng-binding ng-scope" role="button" title="Click here to cancel your changes">Cancel</button>
</div>
</div>

For the sn-record-picker, I used my old friend, the sn-record-picker Helper, but before I got that far, I had to first work out the query. I wanted any CI table (the table name is also the class name), so I was looking for any table that was based on the cmdb_ci table. There may be an easier way to do this, but this works, so I just went with it.

super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54

That’s an ugly, brute force way of doing that, but it gets the job done, which is really all that we are after at this point. The real fun will be putting all of the code behind the screen to both build up the map and then store it somewhere. That’s pretty complicated stuff, and I’m not really sure exactly how I am going to do all of that, so we’ll save that as an exercise for another day.