Static Monthly Calendar, Part III

“Mistakes are the portals of discovery.”
James Joyce

While experimenting with a number of various configurations for my Static Monthly Calendar, I ran into a number of issue that led me to make a few adjustments to the code, and eventually, to actually build a few new parts that I am hoping might come in handy in some future effort. The first problem that I ran into was when I tried to configure a content provider from a scoped app. The code that I was using to instantiate a content provider using the name was this:

var ClassFromString = this[options.content_provider];
contentProvider = new ClassFromString();

This works great for a global Script Include, but for a scoped component, you end up with this:

var ClassFromString = this['my_scope.MyScriptInclude'];

… when what you really need is this:

var ClassFromString = this['my_scope']['MyScriptInclude'];

I started to fix that by adding code to the widget, but then I decided that it was code that would probably be useful in other circumstances, so I ended up creating a separate global component to turn an object name into an instance of that object. That code turned out to look like this:

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

	_root: null,

	setRoot: function(root) {
		this._root = root;
	},

	getInstance: function(name) {
		var instance;

		var scope;
		var parts = name.split('.');
		if (parts.length == 2) {
			scope = parts[0];
			name = parts[1];
		}
		var ClassFromString;
		try {
			if (scope) {
				ClassFromString = this._root[scope][name];
			} else {
				ClassFromString = this._root[name];
			}
			instance = new ClassFromString();
		} catch(e) {
			gs.error('Unable to instantiate instance named "' + name + '": ' + e);
		}

		return instance;
	},

	type: 'Instantiator'
};

This handles both global and scoped components, and also simplified the code in the widget, which turned out to be just this:

contentProvider = instantiator.getInstance(options.content_provider);

Another issue that I ran into was when I tried to inject content that allowed the user to click on an event to bring up some additional details about the event in a modal pop-up. I created a function called showDetails to handle the modal pop-up, and then added an ng-click to the enclosing DIV of the HTML provided by my example content provider call this new function. Unfortunately, the ng-click, which was added to the page with the rest of the provided content, was inserted using an ng-bind-html attribute, which simply copies in the raw HTML and doesn’t actually compile the AngularJS code. I tried various approaches to compiling the code myself, but I was never able to get any of those to work. Then I came across this, which seemed like just the thing that I needed. I thought about installing in in my instance, but then I thought that I had better check first, because it’s entirely possible that it is already in there. Sure enough, I came across the Angular Provider scBindHtmlCompile, which seemed like a version of the very same thing. So I attached it to my widget and replaced by ng-bind-html with sc-bind-html-compile.

Unfortunately, that just put the compiler into an endless loop, which ultimately resulted in filling up the Javascript console with quite a few of these error messages:

Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!

I searched around for a solution to that problem, but nothing that I tried would get around the problem. I ended up going in the opposite direction and swapping out the ng-click for an onclick, which doesn’t need to be compiled. Of course, the onclick can’t see any of the functions inside the scope of the app, so I had to write a stand-alone UI Script to include with a script tag in order to have a function to call. That function is outside of the scope of the app as well, so I ended up turning the script into yet another generic part that uses the element to get you back to the widget:

function functionBroker(id, func, arg1, arg2, arg3, arg4) {
	var scope = angular.element(document.getElementById(id)).scope();
	scope.$apply(function() {
		scope[func](arg1, arg2, arg3, arg4);
	});
}

You pass it the ID of your HTML element, the name of function that is in scope, and up to four independent arguments that you would like to pass to the function. It uses the element to locate the scope, and then uses the scope to find your desired function and passes in the arguments. After saving the new generic script, I went back into the widget and added a script tag to the widget’s HTML to pull the script onto the page.

<script type="text/javascript" src="/function_broker.jsdbx"></script>

Then I added a function to pop open a modal dialog based on a configuration object passed into the function.

$scope.showDetails = function(modalConfig) {
	spModal.open(modalConfig);
};

Now, I just needed something to pop up to see if it all worked. Not too long ago I made a simple widget to show off my rating type form field, and that looked like a good candidate to use just to see if everything was going to work out the way that it should. I pulled up the ExampleContentProvider that I created earlier, and added one more event in the middle of the month that would bring up this repurposed widget when clicked.

if (dd == 15) {
	response += '<div class="event" id="event15" onclick="functionBroker(\'event15\', \'showDetails\', {title: \'Fifteenth of the Month Celebration\', widget:\'feedback-example\', size: \'lg\'});" style="cursor: pointer;">\n';
	response += '  <div class="event-desc">\n';
	response += '    Fifteenth of the Month Celebration\n';
	response += '  </div>\n';
	response += '  <div class="event-time">\n';
	response += '    Party Time\n';
	response += '  </div>\n';
	response += '</div>\n';

The whole thing is kind of a Rube Goldberg operation, but it should work, so let’s light things up and give it a try.

Modal pop-up from clicking on an Event

After all of the failed attempts at making this happen, it’s nice to see the modal dialog actually appear on the screen! It still seems like there has got to be a simpler way to make this work, but until I figure that out, this will do. If you’d like to play around with it yourself, here’s an Update Set that I hope includes all of the right pieces. There are still a few little things that I would like to add one day, so this may not quite be the last you will see of this one.

@mentions in the Service Portal

“Don’t worry if it doesn’t work right. If everything did, you’d be out of a job.”
Mosher’s Law of Software Engineering

There is a nifty feature on the UI side of the Now Platform that allows you to include “mentions” of other platform users in various messages and form fields such as the Comments and Work Notes on the Incident form. To mention another user, you simply type an @ character before typing out their name:

Adding an @mention to a form field

I stole that image from the documentation, which you can find here. It’s a nice feature, and it opens up a number of possibilities for other cool stuff, but it’s only available on the primary UI side. As of yet, this feature has not found its way to the Service Portal. I’m sure that if I were to wait long enough, some future version will resolve that minor shortcoming, but not being one who likes to wait for things, I though that maybe I would try to see if I could make it work myself. How hard could it be?

I started out by digging around in the source code of the Incident form, trying to figure out how everything worked. I located the textarea tag for the comments field and took a look at all of the attributes:

<textarea
  id="activity-stream-comments-textarea"
  aria-label="{{activity_field_1.label}}"
  class="sn-string-textarea form-control"
  placeholder="Additional comments"
  data-stream-text-input="comments"
  ng-required="activity_field_1.mandatory && !activity_field_1.filled"
  ng-model="activity_field_1.value"
  ng-attr-placeholder="{{activity_field_1.label}}"
  sn-sync-with="activity_field_1.name"
  sn-sync-with-value-in-fn="reduceMentions(text)"
  sn-sync-with-value-out-fn="expandMentions(text)"
  mentio=""
  mentio-id="'activity-stream-comments-textarea'"
  mentio-typed-term="typedTerm"
  mentio-require-leading-space="true"
  mentio-trigger-char="'@'"
  mentio-items="members"
  mentio-search="searchMembersAsync(term)"
  mentio-template-url="/at-mentions.tpl"
  mentio-select="selectAtMention(item)"
  mentio-suppress-trailing-space="true"
  sn-resize-height="">
</textarea>

Clearly, everything that started with mentio was related to this feature, so on a whim I decided to search the Interwebs for the term mentio and discovered that the good folks at ServiceNow were using this product to implement this feature. That was actually a nice find, as the site came complete with documentation, which really made figuring all of this out quite a bit easier than I had originally imagined.

I wasn’t sure how many of the parts and pieces needed to make this work were already present in the Service Portal, so my first attempt was just to create a widget with a single textarea form field and add all of these mentio– attributes:

<snh-form-field
  snh-model="c.data.textarea"
  snh-name="textarea"
  snh-type="textarea"
  snh-label="${ment.io Example}"
  snh-help="While entering your text, type @username anywhere in the message. As you type, an auto-suggest list appears with names and pictures of users that match your entries. For example, if you type @t, the auto-suggest list shows the pictures and names of all users with names that start with T. Click the user you want to add and that user's name is inserted into the @mention in the body of your text."
  mentio=""
  mentio-macros="macros"
  mentio-trigger-char="'@'"
  mentio-items="members"
  mentio-search="searchMembersAsync(term)"
  mentio-template-url="/at-mentions.tpl"
  mentio-select="selectAtMention(item)"
  mentio-typed-term="typedTerm"
  mentio-id="'textarea'"
  ng-trim="false"
  autocomplete="off"/>

That at least got me the basic structure with which to experiment, and turned out looking like this:

Basic textarea for @mentions enablement

Of course, typing an @ character did not immediately pop up the expected user selection screen, but I really didn’t expect that at this stage of the game. Three of those mentio attributes in particular seemed to suggest that there was more parts needed to make all of this work:

  mentio-search="searchMembersAsync(term)"
  mentio-template-url="/at-mentions.tpl"
  mentio-select="selectAtMention(item)"

The first and the last I recognized as missing Javascript functions, but I wasn’t sure what that one in the middle could be. A quick check of the ment.io documentation revealed this:

mentio-template-url
Optional. Specifies the template url to use to render the select menu. The template should iterate the items list to present a menu of choices. The items scope property from the mentio-menu is available to iterate within an ng-repeat. The typedTerm scope property from the mentio-menu can be accessed in order to highlight text in the menu. The default template presents a simple menu, and assumes that each object has a property called label.

Armed with that little tidbit of additional knowledge, I went back to the source code of the Incident form, and found this:

<script type="text/ng-template" id="/at-mentions.tpl">
	<div class="dropdown-menu sn-widget sn-mention">
		<ul class="sn-widget-list_v2">
			<li ng-if="items.length > 0 && !items[0].termLengthIsZero" mentio-menu-item="person" ng-repeat="person in items">
				<div class="sn-widget-list-content sn-widget-list-content_static">
					<sn-avatar primary="person" class="avatar-small" show-presence="true"></sn-avatar></div>
				<div class="sn-widget-list-content">
					<span class="sn-widget-list-title" ng-bind-html="person.name"></span>
					<span class="sn-widget-list-subtitle" ng-if="!person.record_is_visible">Cannot see record</span></div></li>
			<li ng-if="items.length === 1 && items[0].termLengthIsZero">
				<div class="sn-widget-list-content">
					<span class="sn-widget-list-title sn-widget-list-title_wrap">Enter the name of a person you want to mention</span></div></li>
			<li ng-if="items.length === 0 && items.loading && visible">
				<div class="sn-widget-list-content sn-widget-list-content_static">
					<span class="sn-widget-list-icon icon-loading"></span></div>
				<div class="sn-widget-list-content">
					<span class="sn-widget-list-title">Loading...</span></div></li>
			<li ng-if="items.length === 0 && !items.loading">
				<div class="sn-widget-list-content">
					<span class="sn-widget-list-title">No users found</span></div></li></ul>
	</div>
</script>

I’ve never seen HTML stored inside of a script tag before, but obviously it works, so I just copied that into the HTML for the widget. Then I hunted down the two missing functions that were referenced in the other attributes, pasted those into the widget’s client script and gave it another try. Still nothing. Now it was time to take a hard look at those functions and see if I couldn’t toss in some alerts here and there to see if I could figure out what was working and what was having issues. I figured that the best place to start was with the function that fetched the data to be display on the pick list. Here is the original script from the Incident page:

$scope.searchMembersAsync = function(term) {
$scope.members = [];
$scope.members.loading = true;
$timeout.cancel(typingTimer);
if (term.length === 0) {
$scope.members = [{
termLengthIsZero: true
}];
$scope.members.loading = false;
} else {
typingTimer = $timeout(function() {
snMention.retrieveMembers($scope.table, $scope.sysId, term).then(function(members) {
$scope.members = members;
$scope.members.loading = false;
}, function () {
$scope.members = [{
termLengthIsZero: true
}];
$scope.members.loading = false;
});
}, 500);
}
};

That’s not very nicely formatted, so it’s a little hard for me to read, but I noticed two things: 1) it was using a component called snMention to fetch the data, and 2) it was passing the function a table and a sys_id in addition to what the person had typed on the screen. The first thing that I did was add snMention as an argument to the client script function along with $timeout, which was also referenced in the script. I also wasn’t on a form associated with a table, so I replaced those two variables with null. That allowed the function to work, but it did not send back any data. Apparently, you have to send in the parameters for a record to which the current user has write authority. I didn’t have one of those, so I decided to try the user’s sys_user record, which actually did the trick. I still wasn’t getting anything on the screen, but at least I could see that I was actually getting results based on what was being entered in the field. That was progress. Here is how the client script turned out after all of that:

function($scope, $timeout, snMention, spModal) {
	var c = this;
	var typingTimer;

	$scope.snMention = snMention;
	$scope.macros = {};
	$scope.mentionMap = {};
	$scope.members = [];
	$scope.theTextArea = '';

	$scope.searchMembersAsync = function(term) {
		$scope.members = [];
		$scope.members.loading = true;
		$timeout.cancel(typingTimer);
		if (term.length === 0) {
			$scope.members = [{
				termLengthIsZero: true
			}];
			$scope.members.loading = false;
		} else {
			typingTimer = $timeout(function() {
				snMention.retrieveMembers('sys_id', c.data.userId, term).then(function(members) {
					$scope.members = members;
					$scope.members.loading = false;
				}, function () {
					$scope.members = [{
						termLengthIsZero: true
					}];
					$scope.members.loading = false;
				});
			}, 500);
		}
	};

	$scope.selectAtMention = function(item) {
		if (item.termLengthIsZero) {
			return (item.name || "") + "\n";
		}
		$scope.mentionMap[item.name] = item.sys_id;
		return "@[" + item.name + "]";
	};
}

After a lot of trial and error (mostly error!), I finally figured out that the reason that I wasn’t getting anything to show up on the screen was that I was missing some really important CSS. Rather than pick through the style sheets and pull out the various bits that I needed, I just added the following to the top of my HTML:

<link rel="stylesheet" type="text/css" href="/styles/heisenberg/heisenberg_all.cssx">
<link rel="stylesheet" type="text/css" href="/css_includes_ng.cssx">

Now we’re cooking with gas!

What do you know … it actually works!

That was a little lazy, as I am sure that I only needed a few odds and ends from those additional style sheets, but this is just an example, so I just pulled in the entire thing, unnecessary bloat and all. Once I got everything working as it should, I wanted to do something with the list of folks who were mentioned in the text, just to show how that part works as well, so I created another widget to pop up in a modal dialog that listed out the people. Now, when you hit the Submit button on this little example, you get this:

List of Users mentioned in the text

In addition to the separate widget to display the names, I had to add one more function to the client side script of the main widget:

$scope.showMentions = function() {
	var mentions = [];
	for (var name in $scope.mentionMap) {
		mentions.push({name: name, sys_id: $scope.mentionMap[name]});
	}
	spModal.open({
		title: 'Users Mentioned',
		widget: 'mentions',
		widgetInput: {mentions: JSON.stringify(mentions)},
		buttons: [
			{label: 'Close', primary: true}
		],
		size: 'lg'
	});
}

There is still quite a bit of clean-up that I would like to do before considering this really ready for prime time, but I achieved my main objective, which was just to see if I could get it to work. It does work, and if I don’t say so myself, it’s a pretty cool feature to add to the toolbox. If you would like to play around with code yourself, here is an Update Set with everything that you will need to pull this off.

Customizing the Data Table Widget, Part V

“Quality is not an act; it is a habit.”
Aristotle

In order to test my Data Table buttons and icons, I’m going to need a way to trigger both options, navigating to a new page and processing the global broadcast. Since my initial test page was configured to use the sys_user table, bouncing over to the User Profile page seems like easiest thing to do to demonstrate the first. But, to demonstrate the second, I’m going to have to create another widget, one that I will build just to prove that the other option works as well. Before we do that, though, let’s set up the button configuration JSON object to create one button and one icon, and have one implement the first option and the other implement the second. That will set things up for our testing.

[
   {
      "name":"button",
      "label":"Button",
      "heading":"Button",
      "color":"primary",
      "page_id":"user_profile",
      "hint":"Clicking this button should take you to the User Profile page"
   },{
      "name":"icon",
      "label":"Icon",
      "heading":"Icon",
      "icon":"user",
      "hint":"Clicking this icon should open a modal pop-up"
   }
]

We can enter that configuration using the Page Designer and clicking on the Edit icon (pencil) to bring up the options screen:

Entering the button configuration in the widget options panel

With that configured, we can actually test the button right away, as that one is configured to branch to another page (user_profile). We can’t test the icon, though, until we build a widget to listen for the broadcast.

So, I went over to the Service Portal Widgets list and clicked on the New button to create a net new widget to listen for the broadcast message. The only things this widget will do is listen and take action, so I didn’t need a Body HTML template and I didn’t need a Server script, so I just left those blank. For the Client controller, I just entered this:

function(spModal, $rootScope) {
	var c = this;
	$rootScope.$on('button.click', function(e, parms) {
		spModal.open({widget: 'user-profile', widgetInput: {sys_id: parms.record.sys_id}}).then(function() {
			//
		});
	});
}

This was about a simple as I could think of and still test out the process. Unfortunately, when I tested it, it didn’t work. It turns out that the stock user-profile widget cannot accept a sys_id from input. Well, that’s easily enough fixed, but I had to clone the user-profile widget to add in the code, and then I had to have my new listener widget launch the cloned snh-user-profile widget instead of the original. Now, that worked.

Modal pop-up opened by example listener widget

The listener widget has no HTML body, so you can pretty much drag it anywhere on the page. Once it shares the page with the customized Data Table widget, it will be able to pick up the broadcast and do whatever it is that you want to do. My example just checks for a ‘button-click’ event, but if you have more than one button on your page, you may also what to check to see which button was clicked before you take any action. I’ll leave that to those of you who want to try all this out on your own.

One unfortunate bit of bad news, though: in the process of testing all of this out, I clicked on one of the column headings to sort the list and my buttons disappeared. That was deflating! That’s why we pull on all of the levers and twist all of the dials, though. It’s important to check everything out thoroughly. Still, I hate to be reminded that I don’t really know what I am doing. It will probably take me a while to dig around, find the source of the problem, and come up with a viable solution. Still, I did promise to publish an Update Set soon, so I think I am going to go ahead and do that, even though this version violates Rule #1. If you don’t mind playing around with something that is obviously broken, you can get the version 1.0 Update Set here. Just be aware that there is a flaw that has yet to be corrected. Speaking of which, I better get busy diagnosing and correcting that problem.

Service Portal Form Fields, Part VIII

“I regret only one thing, which is that the days are so short and that they pass so quickly. One never notices what has been done; one can only see what remains to be done, and if one didn’t like the work it would be very discouraging.”
Maria Sklodowska

Well, I finally resolved the two major annoyances of my little form field project. One was having to include the form name as an attribute when I felt for sure that there must be a way to derive that without having to pass it in explicitly, and the other was related to the validation message behavior. When my error message routine was based on watching $valid, the message did not change even though the reason that it was not $valid changed. Empty required fields would show the message about the field being required, but when you started typing and the value was not yet a valid value, that original message would remain rather than switching to something more appropriate such as explaining to you that the value that you were entering was not a valid value. I tried switching to watching $error instead of $valid, but that didn’t work at all. So, I went out and scoured the Interwebs for an answer to my dilemma.

As it turns out, watching $error really was the right thing to do, but in order for that to work, I had to add true as a third argument to the scope.$watch function call (the first argument is the element to watch and the second is the function to run when something changes). I’m not sure what that third argument is or does, but I now know that if you add it, then it works, so I guess that’s all I really need to know.

As for the form name, after many, many, many different experiments, I finally stumbled across a sequence of random letters, numbers, and symbols that consistently returned the name of the form:

var form = element.parents("form").toArray()[0].name;

Of course, now that it is laid out right there in front of you in plain sight, you can tell right away how intuitively obvious that should have been from the start. Why I didn’t just know that right off of the bat will remain an eternal mystery, but the good news is that we have the secret now, and I can finally remove all of those unneeded snh-form=”form1″ attributes from all of my various test cases. I always felt as if that shouldn’t have been necessary, but I could never quite come up with an approach that would return the name of the form in every instance. Fortunately, I am rather relentless with these kinds of things and I just kept trying things until I finally stumbled upon something that actually worked.

Those were the two major items on my list of stuff that I thought needed to be addressed before we could really call this good enough. I also did a little bit of clean-up in a few other areas as well, just tidying a few little odds and ends up here and there where I thought things could use a little improvement. Here is the full text of the current version of the script that performs all of the magic:

function() {
	var SUPPORTED_TYPE = ['choicelist', 'date', 'datetime-local', 'email', 'inlineradio', 'month', 'number', 'password', 'radio', 'reference', 'tel', 'text', 'textarea', 'time', 'url', 'week'];
	var RESERVED_ATTR = ['ngHide', 'ngModel', 'ngShow', 'class', 'field', 'id', 'name', 'required'];
	var SPECIAL_TYPE = {
		email: {
			title: 'Send an email to {{MODEL}}',
			href: 'mailto:{{MODEL}}',
			icon: 'mail'
		},
		tel: {
			title: 'Call {{MODEL}}',
			href: 'tel:{{MODEL}}',
			icon: 'phone'
		},
		url: {
			title: 'Open {{MODEL}} in a new browser window',
			href: '{{MODEL}}" target="_blank',
			icon: 'pop-out'
		}
	};
	var STD_MESSAGE = {
		email: "Please enter a valid email address",
		max: "Please enter a smaller number",
		maxlength: "Please enter fewer characters",
		min: "Please enter a larger number",
		minlength: "Please enter more characters",
		number: "Please enter a valid number",
		pattern: "Please enter a valid value",
		required: "This information is required",
		url: "Please enter a valid URL",
		date: "Please enter a valid date",
		datetimelocal: "Please enter a valid local date/time",
		time: "Please enter a valid time",
		week: "Please enter a valid week",
		month: "Please enter a valid month"
	};
	return {
		restrict: 'E',
		replace: true,
		require: ['^form'],
		template: function(element, attrs) {
			var htmlText = '';
			var form = element.parents("form").toArray()[0].name;
			var name = attrs.snhName;
			var model = attrs.snhModel;
			var type = attrs.snhType || 'text';
			var required = attrs.snhRequired && attrs.snhRequired.toLowerCase() == 'true';
			var fullName = form + '.' + name;
			var refExtra = '';
			type = type.toLowerCase();
			if (SUPPORTED_TYPE.indexOf(type) == -1) {
				type = 'text';
			}
			if (type == 'reference') {
				fullName = form + "['" + name + " ']";
				refExtra = '.value';
			}
			htmlText += "    <div id=\"element." + name + "\" class=\"form-group\"";
			if (attrs.ngShow) {
				htmlText += " ng-show=\"" + attrs.ngShow + "\"";
			}
			if (attrs.ngHide) {
				htmlText += " ng-hide=\"" + attrs.ngHide + "\"";
			}
			htmlText += ">\n";
			htmlText += "      <div id=\"label." + name + "\" class=\"snh-label\" nowrap=\"true\">\n";
			htmlText += "        <label for=\"" + name + "\" class=\"col-xs-12 col-md-4 col-lg-6 control-label\">\n";
			htmlText += "          <span id=\"status." + name + "\"";
			if (required) {
				htmlText += " ng-class=\"" + model + refExtra + ">''?'snh-required-filled':'snh-required'\"";
			}
			htmlText += "></span>\n";
			htmlText += "          <span title=\"" + attrs.snhLabel + "\" data-original-title=\"" + attrs.snhLabel + "\">" + attrs.snhLabel + "</span>\n";
			htmlText += "        </label>\n";
			htmlText += "      </div>\n";
			if (attrs.snhHelp) {
				htmlText += "      <div id=\"help." + name + "\" class=\"snh-help\">" + attrs.snhHelp + "</div>\n";
			}
			if (type == 'radio' || type == 'inlineradio') {
				htmlText += buildRadioTypes(attrs, name, model, required, type);
			} else if (type == 'choicelist') {
				htmlText += buildChoiceList(attrs, name, model, required);
			} else if (SPECIAL_TYPE[type]) {
				htmlText += buildSpecialTypes(attrs, name, model, required, type, fullName);
			} else if (type == 'reference') {
				htmlText += "      <sn-record-picker field=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></sn-record-picker>\n";
			} else if (type == 'textarea') {
				htmlText += "      <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";
			} else {
				htmlText += "      <input class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
			}
			htmlText += "      <div id=\"message." + name + "\" ng-show=\"(" + fullName + ".$touched || " + fullName + ".$dirty || " + form + ".$submitted) && " + fullName + ".$invalid\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";
			htmlText += "    </div>\n";
			return htmlText;

			function buildRadioTypes(attrs, name, model, required, type) {
				var htmlText = "      <div style=\"clear: both;\"></div>\n";

				var option = null;
				try {
					option = JSON.parse(attrs.snhChoices);
				} catch(e) {
					alert('Unable to parse snh-choices value: ' + attrs.snhChoices);
				}
				if (option && option.length > 0) {
					for (var i=0; i<option.length; i++) {
						var thisOption = option[i];
						if (type == 'radio') {
							htmlText += "      <div>\n  ";
						}
						htmlText += "        <input ng-model=\"" + model + "\" id=\"" + name + thisOption.value + "\" name=\"" + name + "\" value=\"" + thisOption.value + "\" type=\"radio\"" + passThroughAttributes(attrs) + (required?' required':'') + "/> " + thisOption.label + "\n";
						if (type == 'radio') {
							htmlText += "      </div>\n";
						}
					}
				}

				return htmlText;
			}

			function buildChoiceList(attrs, name, model, required) {
				var htmlText = "      <select class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + ">\n";
				var option = null;
				try {
					option = JSON.parse(attrs.snhChoices);
				} catch(e) {
					alert('Unable to parse snh-choices value: ' + attrs.snhChoices);
				}
				htmlText += "        <option value=\"\"></option>\n";
				if (option && option.length > 0) {
					for (var i=0; i<option.length; i++) {
						var thisOption = option[i];
						htmlText += "        <option value=\"" + thisOption.value + "\">" + thisOption.label + "</option>\n";
					}
				}
				htmlText += "      </select>\n";

				return htmlText;
			}

			function buildSpecialTypes(attrs, name, model, required, type, fullName) {
				var title = SPECIAL_TYPE[type].title.replace('MODEL', model);
				var href = SPECIAL_TYPE[type].href.replace('MODEL', model);
				var icon = SPECIAL_TYPE[type].icon;
				var htmlText = "      <span class=\"input-group\" style=\"width: 100%;\">\n";
				htmlText += "       <input class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
				htmlText += "       <span class=\"input-group-btn\" ng-show=\"" + fullName + ".$valid && " + model + " > ''\">\n";
				htmlText += "        <a href=\"" + href + "\" class=\"btn-ref btn btn-default\" role=\"button\" title=\"" + title + "\" tabindex=\"-1\" data-original-title=\"" + title + "\">\n";
				htmlText += "         <span class=\"icon icon-" + icon + "\" aria-hidden=\"true\"></span>\n";
				htmlText += "         <span class=\"sr-only\">" + title + "</span>\n";
				htmlText += "        </a>\n";
				htmlText += "       </span>\n";
				htmlText += "      </span>\n";
				return htmlText;
			}

			function passThroughAttributes(attrs) {
				var htmlText = '';
				for (var name in attrs) {
					if (name.indexOf('snh') != 0 && name.indexOf('$') != 0 && RESERVED_ATTR.indexOf(name) == -1) {
						htmlText += ' ' + camelToDashed(name) + '="' + attrs[name] + '"';
					}
				}
				return htmlText;
			}

			function camelToDashed(camel) {
				return camel.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase();
			}
		},
		link: function(scope, element, attrs, ctls) {
			var form = ctls[0].$name;
			var name = attrs.snhName;
			var fullName = form + '.' + name;
			if (!scope.$eval(fullName)) {
				fullName = form + '["' + name + ' "]';
			}
			var overrides = {};
			if (attrs.snhMessages) {
				overrides = scope.$eval(attrs.snhMessages);
			}
			scope.$watch(fullName + '.$error', function () {
				var elem = scope.$eval(fullName);
				elem.validationErrors = '';
				var separator = '';
				if (elem.$invalid) {
					for (var key in elem.$error) {
						elem.validationErrors += separator;
						if (overrides[key]) {
							elem.validationErrors += overrides[key];
						} else if (STD_MESSAGE[key]) {
							elem.validationErrors += STD_MESSAGE[key];
						} else {
							elem.validationErrors += 'Undefined field validation error: ' + key;
						}
						separator = '<br/>';
					}
				}
			}, true);
		}
	};
}

I already released an almost good enough version that I ended up calling 1.0, so I guess we’ll have to call this one 1.1. You can grab the full Update Set, which now includes the CSS as a separate file instead of being pasted into the test widget like it was in the original version. It would probably be beneficial to include some semblance of documentation at some point, but that will have to wait for a later release.

Service Portal Form Fields, Part VI

“If at first you don’t succeed, call it version 1.0”
Charles Lauller

Well, that was an adventure! Even though I was just about 100% sure that it wouldn’t work, I went ahead and defined yet another type for my snh-form-field tag to experiment with embedding an sn-record-picker. It seemed highly unlikely to me that AngularJS was smart enough to run through the evaluation process on a tag that was rendered in a template that itself was resolved in the evaluation process. But, I like to try things even when I’m pretty sure that they won’t work, so I added reference to my list of supported types and added this to my little test page widget:

        <snh-form-field
          snh-form="form1"
          snh-model="c.data.reference"
          snh-name="reference"
          snh-label="Reference Field"
          snh-type="reference"
          table="'cmn_department'"
          display-field="'name'"
          display-fields="'id'"
          value-field="'sys_id'"
          search-fields="'id,name'"
          default-query="'u_active=true"
          page-size="100"
          snh-required="true"/>

To my utter amazement, it actually worked! I did not expect that. I was pleasantly surprised to see that it worked perfectly fine, and I was very happy that I had decided to give it shot before trying to figure out some other way to incorporate this capability in my own directive. There were, of course, a few little adjustments needed here and there, but hey — it was pretty cool to see that it actually worked without me having to do much of anything. I was both astonished and elated. Elation, though, is a temporary state, and things really started to go down from there.

To start with, it just looked a little funky. It was like a field within a field, but not completely, with one element bleeding over the bottom edge of the other. Something just wasn’t right somewhere.

First attempt at setting up a reference field

That one wasn’t that hard to figure out though. I had added the snh-form-control class to the sn-record-picker tag, but the sn-record-picker generates its own class attribute on a different level, so the two were redundant as well as conflicting. I removed my class attribute and that solved that problem. But that was really an insignificant problem compared to what I noticed next: the field wasn’t validating.

The required field indicator was grey instead of red, no matter whether or not you had selected anything from the drop-down. And no error messages appeared no matter what you did. It was as if validation wasn’t turned on for that field at all. Then I remembered that the model for an sn-record-picker is not a string containing the value selected, but an object containing a property that contains the value selected, appropriately named value. Where the value of every other type of field was <form-name>.<field-name>, the value of an sn-record-picker is <form-name>.<field-name>.value. That meant putting in some conditional code just for the reference type fields, which I had not really had to do for any other types up to this point.

Unfortunately, things still were not working after all of that. Now I was starting to get a little frustrated because the picker itself just worked like a charm right out of the gate, which I absolutely did not expect, but the field validation, which I thought would be the one part to work just fine, still wasn’t working at all. I kept digging, though, and eventually I figure out that when the sn-record-picker renders the name attribute, it throws in an extra little trailing space at the end of field name. Instead of name=”reference” I ended up with name=”reference “. Well, that little extra, unwelcome, trailing space means that <form-name>.<field-name> doesn’t reference that field after all. You need to use <form-name>[‘<field-name-plus-one-space>’] because you can’t represent that trailing space without using the square bracket notation instead of the dot notation. So, I ended up adding yet more type-specific code. I don’t know if that extra trailing space is just an unfortunate mistake or it was put in by design, but if it was a mistake, I hope they don’t fix it one day, because now I am relying on it being there for things to work.

After all of those changes, though, it still wasn’t validating. Things were starting to progress from frustration to irritation. Every time that I thought that I had found and fixed the problem, I would try it out expecting things to be fixed, and it still wouldn’t be working. After a lot of head scratching and debugging alerts and trial and error, I finally was able to figure out that the validation messages only appear once the field had been $touched, and the sn-record-picker fields are never $touched. They can get $dirty, but for some reason, they are never $touched. So I modified my condition on showing the validation errors be $touched or $dirty, and finally, everything worked as it should. Finally.

So here is the way things look so far:

function() {
	var SUPPORTED_TYPE = ['choicelist', 'date', 'datetime-local', 'email', 'inlineradio', 'month', 'number', 'password', 'radio', 'reference', 'tel', 'text', 'textarea', 'time', 'url', 'week'];
	var RESERVED_ATTR = ['ngHide', 'ngModel', 'ngShow', 'class', 'field', 'id', 'name', 'required'];
	var STD_MESSAGE = {
		email: "Please enter a valid email address",
		max: "Please enter a smaller number",
		maxlength: "Please enter fewer characters",
		min: "Please enter a larger number",
		minlength: "Please enter more characters",
		number: "Please enter a valid number",
		pattern: "Please enter a valid value",
		required: "This information is required",
		url: "Please enter a valid URL",
		date: "Please enter a valid date",
		datetimelocal: "Please enter a valid local date/time",
		time: "Please enter a valid time",
		week: "Please enter a valid week",
		month: "Please enter a valid month"
	};
	return {
		restrict: 'E',
		replace: true,
		require: ['^form'],
		template: function(element, attrs) {
			var htmlText = '';
			var form = attrs.snhForm;
			var name = attrs.snhName;
			var model = attrs.snhModel;
			var type = attrs.snhType || 'text';
			var required = attrs.snhRequired && attrs.snhRequired.toLowerCase() == 'true';
			var fullName = form + '.' + name;
			var refExtra = '';
			type = type.toLowerCase();
			if (SUPPORTED_TYPE.indexOf(type) == -1) {
				type = 'text';
			}
			if (type == 'reference') {
				fullName = form + "['" + name + " ']";
				refExtra = '.value';
			}
			htmlText += "    <div id=\"element." + name + "\" class=\"form-group\"";
			if (attrs.ngShow) {
				htmlText += " ng-show=\"" + attrs.ngShow + "\"";
			}
			if (attrs.ngHide) {
				htmlText += " ng-hide=\"" + attrs.ngHide + "\"";
			}
			htmlText += ">\n";
			htmlText += "      <div id=\"label." + name + "\" class=\"snh-label\" nowrap=\"true\">\n";
			htmlText += "        <label for=\"" + name + "\" class=\"col-xs-12 col-md-4 col-lg-6 control-label\">\n";
			htmlText += "          <span id=\"status." + name + "\"";
			if (required) {
				htmlText += " ng-class=\"" + model + refExtra + ">''?'snh-required-filled':'snh-required'\"";
			}
			htmlText += "></span>\n";
			htmlText += "          <span title=\"" + attrs.snhLabel + "\" data-original-title=\"" + attrs.snhLabel + "\">" + attrs.snhLabel + "</span>\n";
			htmlText += "        </label>\n";
			htmlText += "      </div>\n";
			if (attrs.snhHelp) {
				htmlText += "    <div id=\"help." + name + "\" class=\"snh-help\">" + attrs.snhHelp + "</div>\n";
			}
			if (type == 'radio' || type == 'inlineradio') {
				htmlText += buildRadioTypes(attrs, name, model, required, type);
			} else if (type == 'choicelist') {
				htmlText += buildChoiceList(attrs, name, model, required);
			} else if (type == 'reference') {
				htmlText += "      <sn-record-picker field=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></sn-record-picker>\n";
			} else if (type == 'textarea') {
				htmlText += "      <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";
			} else {
				htmlText += "      <input class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
			}
			htmlText += "      <div id=\"message." + name + "\" ng-show=\"(" + fullName + ".$touched || " + fullName + ".$dirty) && " + fullName + ".$invalid\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";
			htmlText += "     </div>\n";
			return htmlText;

			function buildRadioTypes(attrs, name, model, required, type) {
				var htmlText = "      <div style=\"clear: both;\"></div>\n";

				var option = null;
				try {
					option = JSON.parse(attrs.snhChoices);
				} catch(e) {
					alert('Unable to parse snh-choices value: ' + attrs.snhChoices);
				}
				if (option && option.length > 0) {
					for (var i=0; i<option.length; i++) {
						var thisOption = option[i];
						if (type == 'radio') {
							htmlText += "      <div>\n  ";
						}
						htmlText += "        <input ng-model=\"" + model + "\" id=\"" + name + thisOption.value + "\" name=\"" + name + "\" value=\"" + thisOption.value + "\" type=\"radio\"" + passThroughAttributes(attrs) + (required?' required':'') + "/> " + thisOption.label + "\n";
						if (type == 'radio') {
							htmlText += "      </div>\n";
						}
					}
				}

				return htmlText;
			}

			function buildChoiceList(attrs, name, model, required) {
				var htmlText = "      <select class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + ">\n";
				var option = null;
				try {
					option = JSON.parse(attrs.snhChoices);
				} catch(e) {
					alert('Unable to parse snh-choices value: ' + attrs.snhChoices);
				}
				htmlText += "        <option value=\"\"></option>\n";
				if (option && option.length > 0) {
					for (var i=0; i<option.length; i++) {
						var thisOption = option[i];
						htmlText += "        <option value=\"" + thisOption.value + "\">" + thisOption.label + "</option>\n";
					}
				}
				htmlText += "      </select>\n";
				return htmlText;
			}

			function passThroughAttributes(attrs) {
				var htmlText = '';
				for (var name in attrs) {
					if (name.indexOf('snh') != 0 && name.indexOf('$') != 0 && RESERVED_ATTR.indexOf(name) == -1) {
						htmlText += ' ' + camelToDashed(name) + '="' + attrs[name] + '"';
					}
				}
				return htmlText;
			}

			function camelToDashed(camel) {
				return camel.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase();
			}
		},
		link: function(scope, element, attrs, ctls) {
			var form = ctls[0].$name;
			var name = attrs.snhName;
			var fullName = form + '.' + name;
			if (!scope.$eval(fullName)) {
				fullName = form + '["' + name + ' "]';
			}
			var overrides = {};
			if (attrs.snhMessages) {
				overrides = scope.$eval(attrs.snhMessages);
			}
			scope.$watch(fullName + '.$valid', function (isValid) {
				var elem = scope.$eval(fullName);
				elem.validationErrors = '';
				var separator = '';
				if (!isValid) {
					for (var key in elem.$error) {
						elem.validationErrors += separator;
						if (overrides[key]) {
							elem.validationErrors += overrides[key];
						} else if (STD_MESSAGE[key]) {
							elem.validationErrors += STD_MESSAGE[key];
						} else {
							elem.validationErrors += 'Undefined field validation error: ' + key;
						}
						separator = '<br/>';
					}
				}
			});
		}
	};
}

I’m still not happy about having to specify the form name, still need to do something about the wrong validation message showing up in certain circumstances, and I still haven’t tested all of the support types just yet, but I think it just might be complete enough to go ahead and throw an initial version into an Update Set for anyone who might want to play along at home. There is still a lot that I would like to do before I’m ready to call it really good, but it does work, so there is that. As the man who jumped off of the high-rise building was heard saying as he passed each floor, “So far, so good …”

Yet Even More Fun with JDBC Data Sources

“We have a strategic plan; it’s called doing things.”
Herb Kelleher

Recently I had the need to write to an external database on demand in much the same way as I had been reading from an external database on demand earlier. I was able to hack the Scheduled Import process to fetch a specific record, so my thought was that there must be some kind of equivalent Scheduled Export process that I might be able to bend to my will to satisfy my current requirement. Unfortunately, the Scheduled Export process is based on dropping a file somewhere, not on writing to an external database. You have a lot of options as to what type of file it is, but it is still just a file, and I wanted to write to a database table.

So, then I looked into lighting up the JDBCProbe, which I did without too much difficulty, but after digging into all of its various capabilities, I realized that it really didn’t have that much more to offer than the techniques that I had already been using. You still have to create a Data Source, you still have to configure the job to run, you still have to launch the job, and you still have to monitor the job for completion and successful execution. Since I had already pretty much worked all of that out with the Scheduled Import process, decided that I would just stick with that.

The difference, of course, was that this time I wanted to write, not read. Fortunately, that problem can be solved with SQL, and nothing else about my original hack really needed to change. Scheduled Imports expect the end result of the SQL execution to be a Result Set, which is the outcome of an SQL query. However, a query is not the only way to produce a Result Set. SQL Insert and Update statements can also produce a Result Set when you use the Output clause. The Output clause returns specified values such as a generated Identity key back in the form of a Result Set. By leveraging the Output clause, you can build an SQL statement that will both update the external database and return a Result Set all in one operation.

Consider the following example SQL:

INSERT
   EMPLOYEE,
   INCIDENT_ID,
   DATE,
   HOURS_WORKED
OUTPUT
   INSERTED.ID,
   INSERTED.EMPLOYEE,
   INSERTED.INCIDENT_ID,
   INSERTED.DATE,
   INSERTED.HOURS_WORKED
INTO
   TIME_CARD
VALUES(
   'snhackery',
   'INC0000456',
   getdate(),
   6.5)

This SQL statement will insert the specified values into the external database table, and then return the inserted values, along with the newly generated ID on the external table, back to ServiceNow in the form of a Result Set. To the Scheduled Import job, this response is indistinguishable from the response received from a standard SELECT statement. Now, you still have to have a somewhat useless table on the ServiceNow end of things to receive the returned data, even though all you really wanted to do was to write to a table in a remote database, but that table can serve as a log of all records written out to the external destination.

It is possible that we could have accomplished this without the need for the local ServiceNow table by utilizing the JDBCProbe, but this happens to work, so that’s good enough to satisfy Rule #1.

Reference Type System Properties, Part III

“Everything should be made as simple as possible, but not simpler.”
Albert Einstein

Last time, we got far enough along in the System Property value page modifications to demonstrate that we could replace the rendered input element with something else of our own design. Not having a design of our own for an adequate replacement, we implemented the popular creative avoidance strategy by working on all of the other parts and pieces first until we finally ran out of other parts and pieces. Now it is time to come up with a plan and finish this little project up.

I have to admit that I’m not all that proud of what I eventually came up with, but it does satisfy Rule #1, so at this point, I’ll take it and call it good. I tried a number of other things first, but none of those really got me what I wanted, so here we are. The basic plan is pretty simple, and consists of two parts: 1) a hidden input element to replace the text input element so that it can be submitted with the form, and 2) an iframe into which we will put our new input element via a stand-alone page designed for that purpose. I don’t really like the iframe approach, but it does have the benefit of being independently rendered, which gives us the opportunity to leverage the snRecordPicker for our input, which we really cannot do by simply modifying the main page directly after it has been delivered.

So let’s start out with the script that will build the HTML that we will use to replace the original text input element:

function buildHTML(prop) {
	var html = "";
	html += "<input type=\"hidden\" id=\"" + prop.property + "\" name=\"" + prop.property + "\"/>\n";
	html += "<div style=\"width: 100%; height: auto;\">\n";
	html += " <iframe id=\"frame." + prop.property + "\" src=\"/$sp.do?id=reference_properties&sysparm_property=" + prop.property + "&sysparm_table=" + prop.tableName + "&sysparm_column=" + prop.column + "&sysparm_value=" + prop.value + "\" style=\"border: none; height: 65px; width: 100%;\"></iframe>\n";
	html += "</div>\n";
	return html;
}

The hidden input element virtually replaces the original text input element, having the same name and same id. The iframe element is pretty vanilla stuff as well; the only thing of interest really is the src parameter, which points to the Portal Page that we are about to create, and passes along all of the various values needed to make the page do what we want. The Portal Page itself is just a single page with a single container filled with a single widget. The page is not really worth looking at, so let’s just jump right into the widget, as that’s where all of the magic happens. Here is the HTML:

<div id="pickerdiv">
  <sn-record-picker field="field" table="c.data.table" display-field="c.data.column" value-field="'sys_id'" search-fields="c.data.column" page-size="c.data.pageSize"></sn-record-picker>
</div>

Not much to see there, either. It’s just your standard snRecordPicker with pretty much every attribute defined by a variable. We’ll snag the values for those variables off of the URL for the page, which we populated when we constructed the src attribute for the iframe tag. The widget’s client-side script does most of the heavy lifting here:

function($scope, $location) {
	var c = this;
	var qp = $location.search();
	c.data.property = qp.sysparm_property;
	c.data.table = qp.sysparm_table;
	c.data.column = qp.sysparm_column;
	c.data.pageSize = 20;
	c.data.fieldValue = '';
	c.data.fieldDisplayValue = '';
	if (qp.sysparm_page_size) {
		c.data.pageSize = qp.sysparm_page_size;
	}
	if (qp.sysparm_value) {
		c.data.fieldValue = qp.sysparm_value;
		c.server.update().then(function(response) {
			c.data.fieldDisplayValue = response.fieldDisplayValue;
			$scope.field = {
				displayValue: c.data.fieldDisplayValue,
				value: c.data.fieldValue,
				name: 'field'
			};
		});		
	} else {
		$scope.field = {
			displayValue: '',
			value: '',
			name: 'field'
		};
	}
	$scope.$on('field.change', function(evt, parms) {
		if (parms.field.name == 'field') {
			parent.updateReferenceProperty(c.data.property, parms.field.value);
		}
	});
}

The only reason for the server-side script is to fetch the display value of the property if the property is valued at the time that the page is delivered to the browser.

(function() {
	if (input) {
		if (input.fieldValue) {
			var gr = new GlideRecord(input.table);
			gr.get(input.fieldValue);
			data.fieldDisplayValue = gr.getDisplayValue(input.column);
		} else {
			data.fieldDisplayValue = '';
		}
	}
})();

That’s about all there is to it. For every property on the page where Type=reference, the standard text input element is replaced with a hidden input element and an iframe, and inside the iframe is a ServiceNow Service Portal page that contains a single widget containing a single snRecordPicker. The parameters for the picker are passed from the iframe to the portal page via URL parameters, which are picked up by the widget and used to configure the snRecordPicker. All changes to the snRecordPicker are copied over to the hidden input field, so when the form is submitted, the selected value is sent to the server and posted to the database.

There was a minor problem with this initial version when trying to figure out the optimum height for the iframe. The height of the snRecordPicker depends on whether or not the drop-down list of choices is present, and I couldn’t find a CSS-centric way of having the iframe automatically adjust for the change in height, nor could I find a way to have the drop-down list of selectable choices overlay the content below, which is outside of the iframe. Finally, I resorted to plain old Javascript, and set up a variable called c.data.expanded to indicate whether or not the pick list was present on the screen. With a little view selection source magic, I was able to figure out that the component to watch had an id of select2-drop-mask, and so I modified the widget’s client-side code to check the required iframe height every second and adjust if needed:

function($scope, $location) {
	var c = this;
	var qp = $location.search();
	c.data.property = qp.sysparm_property;
	c.data.table = qp.sysparm_table;
	c.data.column = qp.sysparm_column;
	c.data.pageSize = 20;
	c.data.fieldValue = '';
	c.data.fieldDisplayValue = '';
	c.data.expanded = false;
	if (qp.sysparm_page_size) {
		c.data.pageSize = qp.sysparm_page_size;
	}
	if (qp.sysparm_value) {
		c.data.fieldValue = qp.sysparm_value;
		c.server.update().then(function(response) {
			c.data.fieldDisplayValue = response.fieldDisplayValue;
			$scope.field = {
				displayValue: c.data.fieldDisplayValue,
				value: c.data.fieldValue,
				name: 'field'
			};
		});		
	} else {
		$scope.field = {
			displayValue: '',
			value: '',
			name: 'field'
		};
	}
	$scope.$on('field.change', function(evt, parms) {
		if (parms.field.name == 'field') {
			parent.updateReferenceProperty(c.data.property, parms.field.value);
		}
	});
	checkHeight();
	function checkHeight() {
		var elem = document.getElementById('select2-drop-mask');
		if (elem) {
			if (elem.style.display == 'none') {
				if (c.data.expanded) {
					c.data.expanded = false;
					setHeight('65px');
				}
			} else {
				if (!c.data.expanded) {
					c.data.expanded = true;
					setHeight('300px');
				}
			}
		}
		setTimeout(checkHeight, 1000);
	}
	function setHeight(newHeight) {
		parent.updateFrameHeight(c.data.property, newHeight);
	}
}

Once that code was in place, the unexpanded state looked like this:

Modified input element with choices collapsed

… and the expanded state looked like this:

Modified input element with choices expanded

It still disturbs my sense of The Way Things Ought To Be for the left-hand edge of the revised input element not to line up with the left-hand edge of all of the other original input elements, but a fix to that was not readily apparent to me, so I have managed to let that go for now and move on to more important things. One day, though, I am going to figure out a way to fix that!

Just to recap, we modified a choice list and added an additional field to a table to provide the capability to define properties of type reference. We then created a UI Script and a Script Include so that we could replace the original input element on the property UI page, and then we created a Service Portal page and associated widget to provide the replacement for the original input element. As soon as I get a chance, I will see if I can wrap all of that up into a single Update Set and get it posted out here in case anyone wants just grab the whole package. All in all, it was a fun little project, but one that I hope to throw away one day when ServiceNow actually supports reference type properties right out of the box and having this little tweak is no longer needed.

Update: Well, it took a little longer than I had hoped to get around to this, but here is that Update Set finally.

Fun with the User Menu

“Any sufficiently advanced technology is indistinguishable from magic.”
Arthur C. Clarke

In the upper right-hand corner of the ServiceNow UI, you can click on your name and a little menu will drop down with a series of choices:

The ServiceNow User Menu

There is something similar on a ServiceNow Portal, with similar options. To easily move back and forth between the main Portal and the primary UI, I wanted to add an option to each environment’s User Menu to navigate to the other environment. On the Portal, this was easy enough to do by editing the HTML on the Stock Header. In the primary UI, however, this was not quite so easy.

The content of the drop-down User Menu is yet another one of those things that is not stored in the system database, but is rather an integral part of the product itself. As far as I can tell, there is no place to go where you can edit these options. But you know how that goes: if you can’t change what gets sent to the page, that doesn’t mean you can’t alter what is on the page once the page has been delivered. With a little View Source investigating, we can see that the menu is an HTML unordered list (<ul>) with a class of dropdown-menu. That’s enough to get a handle on things and build a quick little UI Script:

addLoadEvent(function() {
	try {
		var ssp = (window.top.$j("#self_service_link").length == 0);
		if (ssp) {
			var sspItem = "<li><a href='/sp' id='self_service_link'>" + getMessage("Self Service Portal") + "</a></li>";
			var menuList = window.top.$j("ul.dropdown-menu");
			if (menuList.length > 0) {
				menuList.prepend(sspItem);
			}
		}
	} catch(e) {
		//
	}
});

The script is pretty straightforward. First we check to make sure that we haven’t already stuffed the extra item out there (sometimes these things have a way of running more than one time), and if we haven’t, then we create the new menu item, locate the existing list, and prepend the new item to the existing list. After this has been activated, the menu now looks like this:

The modified User Menu

I was really quite proud of myself there for a while, and then I realized that on the Portal, the Profile option was still the first option and the ServiceNow option was the second option. By using the prepend function to insert the new menu item, I made it the first item on the menu instead of the second. That really disturbs my sense of The Way Things Ought To Be, so I needed to figure out how to make it the second option and not the first.

It’s true that I could have just reversed things on the Portal side of the house, which is much easier to manipulate, but that disturbs my sense of The Way Things Ought To Be as well. On a User Menu, the Profile link should always be the first and the Logout link should always be the last. Always. I can’t explain it, but that’s just The Way Things Ought To Be. Period.

I’m not going to reveal how many different things that I tried in an effort to insert the new item into the second slot, but let’s just say it’s more than I’d really care to discuss. In the end, I was able to cut and paste some code that I found out on the Interwebs and it did the trick, so that was good enough for me. Don’t ask me to explain how it works, because I haven’t a clue. Although I am pretty handy with Javascript, I don’t know much about AngularJS and I know even less about jQuery. This line of code is pure jQuery voodoo magic as far as I can tell, but it does satisfy rule #1: it works. Yes, that’s unquestionably Cargo Cult Coding, but I’ve never been too proud to leverage someone else’s code, even when I can’t possibly begin to fully understand it myself. You can see the updated script here:

addLoadEvent(function() {
	try {
		var ssp = (window.top.$j("#self_service_link").length == 0);
		if (ssp) {
			var sspItem = "<li><a href='/sp' id='self_service_link'>" + getMessage("Self Service Portal") + "</a></li>";
			var menuList = window.top.$j("ul.dropdown-menu");
			if (menuList.length > 0) {
				window.top.$j("ul.dropdown-menu li:eq(0)").after(sspItem);
			}
		}
	} catch(e) {
		//
	}
});

Now, I can actually look at the menu again without those uncomfortable Rain Man impulses to set things right:

The properly modified User Menu

Well, that’s it for today. If you want a copy to play around with, you can get one here.