Generic Feedback Widget

“Feedback is the breakfast of champions.”
Ken Blanchard

The other day when I was testing out my Static Monthly Calendar I needed a page to demonstrate the modal pop-up, so I grabbed a page that I had created to show off my feedback form field. That got me to thinking that I would be nice to have a universal widget to not only collect new feedback on an item, but to also display all of the feedback that had been collected so far. This is something that ServiceNow already does quite well for Knowledge documents, but that isn’t really universal, and can’t be applied to any other component. Still, it’s a nice model, and one that I wanted to emulate in a way that could be applied to other things.

Typical feedback section of a Knowledge document

Previous comments are listed in reverse chronological order followed by an input form where you can enter new comments. This would be the layout of my proposed widget, with the intent that you could use it for any type of entity, and not just Knowledge. I looked at the underlying table structure for this feature, and the data is eventually stored in two places, a Knowledge table called kb_feedback, and the Live Feed table, live_message. After looking into both, I decided that I could accomplish what I had in mind with the live_message table alone, so I cracked open a shiny new widget that I called generic_feedback and got to work.

I like to solve one problem at a time and then move on to the next one, so I started out with the basic structure of the HTML for the widget, basically copying the original from the Knowledge page:

<div ng-repeat="item in data.feedback">
  <div class="snc-kb-comment-by" style="margin-top: 10px;">
    <img src="/images/icons/feedback.gifx" alt="Feedback" aria-label="Feedback">
    <span class="snc-kb-comment-by-title">
      Posted by <a class="snc-kb-comment-by-user" href="?id=user_profile&table=sys_user&sys_id={{item.userSysId}}">{{item.userName}}</a>
      <span class="snc-kb-comment-by-title">{{item.dateTime}}</span>
    </span>
  </div>
  <div>
    <span class="snc-kb-comment-by-text" ng-bind-html="item.comment"></span>
  </div>
</div>

My only significant deviation from the original was to make the name of the person leaving the comment a link to that person’s User profile page. Other than that, it pretty much followed the original layout.

Based on my data references, I was going to need an array of objects with userSysId, userName, dateTime, and comment properties. These values would all come from the live_message table, but first I needed to figure out how to select the specific messages for the particular item that was the subject of the page. One of the properties of the live_message table is labeled Conversation, and is a reference to another table, live_group_profile. The live_group_profile table contains two properties, table and document, that we can leverage to point to a specific record on a specific table, linking our feedback to pretty much anything in the ServiceNow database. Using this approach, to find all of the feedback for a particular entity, you need the name of the table and the sys_id of the record, and with that information, you can find the live_group_profile record that identifies the “conversation” about that entity. On the server side, that code looks something like this:

var conv = new GlideRecord('live_group_profile');
conv.addQuery('table', data.table);
conv.addQuery('document', data.sys_id);
conv.query();
if (conv.next()) {
	data.convId = conv.getValue('sys_id');
	var fb = new GlideRecord('live_message');
	fb.addQuery('group', data.convId);
	fb.orderByDesc('sys_created_on');
	fb.query();
	while(fb.next()) {
		var feedback = {};
		feedback.userSysId = fb.getValue('profile');
		feedback.userName = fb.getDisplayValue('profile');
		feedback.dateTime = fb.getValue('sys_created_on');
		feedback.comment = fb.getDisplayValue('message');
		data.feedback.push(feedback);
	}
}

Before we can gather up all of the feedback, though, we have to establish the name of the table and the sys_id of the record. For the purpose of this particular widget, we will require that this information be passed in the form of URL parameters. These are the very same URL parameters that drive the Dynamic Service Portal Breadcrumbs (which I also included on the page), and are pretty much a standard in the Service Portal, so that shouldn’t be too onerous of a requirement. The code to pull those values in from the URL and validate them looks like this:

if (input) {
	data.table = input.table;
	data.sys_id = input.sys_id;
} else {
	data.table = $sp.getParameter('table');
	data.sys_id = $sp.getParameter('sys_id');
}
if (data.table && data.sys_id) {
	var gr = new GlideRecord(data.table);
	if (gr.isValid()) {
		if (gr.get(data.sys_id)) {
			data.tableLabel = gr.getLabel();
			data.recordLabel = gr.getDisplayValue();
			...
		}
	}
}

This doesn’t give us a way to enter new feedback, but we have enough now to give things a try, so let’s create a page, drag our new widget onto the page, and see how things are looking so far.

First test of feedback layout using existing Change Record comments

Well, overall it didn’t turn out too bad, but I can see right away a number of things that need a little work. For one, I don’t really like the date format. That definitely needs some improvement. Also, my links don’t work for the User Profile page. That’s because the profile column in the live_message table contains the sys_id of the live_profile record, not the sys_user record. That’s not going to work. It’s always something!

Still, I like the way things are shaping up. Next time out, let’s see if we can’t clean up all of those pesky little issues and then work on capturing new comments and posting them back to the database.

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.

Static Monthly Calendar, Part II

“Life affords no higher pleasure than that of surmounting difficulties, passing from one step of success to another, forming new wishes and seeing them gratified.”
Samuel Johnson

Now that we have the empty shell of a static monthly calendar, we can focus our attention on the integration of dynamic, use-specific content. While I have visions of occupying the entirety of the screen real estate devoted to any particular day with colors and graphics and any content that can be conceived by the provider, for now, I am going to stick with the content model that accompanied the template and focus on the mechanics of integrating the content with the calendar. At this juncture, I will leave it to the reader or some future installment to address the full potential of nature of the content, and will instead, devote this writing to the method used to deliver the content to the calendar.

My approach to integrating user-specified content with the generic calendar shell is to utilize a user-defined content provider, which is simply a Script Include that implements a specific function that takes a date as an argument and returns the content in HTML format. To create a simple example, I used the existing sample content from the original template and created a simple script that I called ExampleContentProvider:

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

	getContent: function(yy, mm, dd) {
		var response = '';

		if (dd == 2) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Career development @ Community College room #402\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    2:00pm to 5:00pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		} else if (dd == 7) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Group Project meetup\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    6:00pm to 8:30pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		} else if (dd == 14) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Board Meeting\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    1:00pm to 3:00pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		} else if (dd == 22) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Conference call\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    9:00am to 12:00pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		} else if (dd == 25) {
			response += '<div class="event">\n';
			response += '  <div class="event-desc">\n';
			response += '    Conference Call\n';
			response += '  </div>\n';
			response += '  <div class="event-time">\n';
			response += '    1:00pm to 3:00pm\n';
			response += '  </div>\n';
			response += '</div>\n';
		}

		return response;
	},

	type: 'ExampleContentProvider'
};

A real content provider would, of course, use some database table or outside source to obtain the raw data from which the HTML would then be generated, but that’s pretty standard stuff that we don’t really need to get into here. Our ExampleContentProvider is simply a means to demonstrate the process of merging the dynamic content with the static calendar. How and from where you obtain your actual content is clearly up to you, but the method that we have implemented will send your process a year, a month, and a day, and will expect your process to send back the appropriate HTML for that specific day in return. You can see the process in action from these new lines in the server-side code:

if (contentProvider && contentProvider.getContent) {
	thisDay.content = contentProvider.getContent(data.year, data.month, thisDay.date);
}

The content provider is optional, so we need to check to make sure that it is there, and we also need to check to make sure that it has the required getContent method, but if all is well in that regard, we set the content for the day to the HTML returned by getContent method of the configured content provider. On the HTML side of things, then, we bind the HTML to the day using an ng-if and an ng-bind-html attribute.

<ul ng-repeat="w in data.week" class="days">
  <li ng-repeat="d in w" class="day" ng-class="{'other-month':!d.date}">
    <div class="date" ng-if="d.date">{{d.date}}</div>
    <div ng-if="d.content" ng-bind-html="d.content"></div>
  </li>
</ul>

All we need to do now is to get the user-provided content provider passed into the calendar widget somehow. This we can do with a widget option, and to make that happen, we need to add an Option Schema:

[{
    "hint": "Class Name of Script Include that provides content",
    "name": "content_provider",
    "default_value": "",
    "label": "Content Provider",
    "type": "string"
}]

Once the Option Schema is in place, clicking on the pencil icon of the widget while in the Portal Page Designer will open up the Options dialog where you can now enter the name of your content provider:

Specifying the content provider on the widget options dialog

Once specified as an option, you can pick it up in the server-side code:

if (options && options.content_provider) {
	var contentProviderName = options.content_provider;
}

Of course, you really aren’t interested in the name of the content provider … you want an actual instance of the Script Include that you can use to invoke the getContent method. For that, we need to instantiate the object using the name:

var contentProvider;
if (options && options.content_provider) {
	try {
		var ClassFromString = this[options.content_provider];
		contentProvider = new ClassFromString();
	} catch(e) {
		gs.error('Unable to instantiate Content Provider named "' + options.content_provider + '": ' + e);
	}
}

Don’t ask me to explain how all of that works, but I can tell you that it does, in fact, work. If you provide the name of an actual Script Include, it will create a usable instance of it, If you provide anything else, it logs an error, and then it goes on as if you never attempted to configure a content provider.

In addition to specifying the content provider via the Portal Page Designer, you can also specify it when including the calendar widget in another widget. In that case, the code would look something like this:

data.calendarWidget = $sp.getWidget('view-only-monthly-calendar', {content_provider: 'ExampleContentProvider'});

Either way, you have to grab the name from the options and instantiate an actual object before you can use it. Well, that’s just about it for the changes, so the next thing to do will be to bring it up and see how it all works.

Calendar with content provided by external content provider

Not bad! You will obviously want to make your own content provider and get your content from the applicable source, but the calendar widget itself should work for just about any content from any source. There are still a few cosmetic tweaks that I wouldn’t mind doing here and there, but it works, so that’s good enough for now. For those of you who might be interested, here is an Update Set.

Static Monthly Calendar

“Do not wait; the time will never be ‘just right.’ Start where you stand, and work with whatever tools you may have at your command, and better tools will be found as you go along.”
George Herbert

Recently, I needed to display a monthly calendar with a few days singled out as noteworthy. That got me to thinking, as it usually does, that it would be nice to have the empty shell calendar as a separate component so that it could be re-utilized for other potential purposes. Aside from my own unique requirements, I could see where someone might want to have something like a payroll calendar filled with holidays and paydays or an event calendar with the line-up of live music each evening. Maybe you just want to call out National Step in a Puddle and Splash Your Friends Day or you just want to be able look at a calendar and see when the cafeteria is serving chipped beef on toast with steamed carrots, Tator Tots, and a butterscotch pudding cup. I have no idea what someone might want to do with a configurable calendar, but it seems like the possibilities could be endless, so it might be worth having a reusable part.

And maybe such a part already exists. ServiceNow actually has a built-in calendar that is used quite extensively, so that seemed like a good place to start. Like many ServiceNow components, it is its own Open Source project called, appropriately enough, Full Calendar. It is quite full, indeed, and has a lot of very cool features, but I was looking for something basic and read-only, and for my purposes, I was gong to have to figure out how to turn most of those features off. I just wanted a static, single-month view that was uneditable and not interactive in any way, except for maybe the ability to click on a date and pull up a little more information. I played around with it for a while, but in the end, I decided that it was going to be more work to turn off a bunch of capabilities than it would be to start off with something much more simplistic.

So then I scoured the Interwebs for a basic HTML calendar template, of which there are many, and finally settled on this one:

Calendar template from responsivedesign.is

It pretty much had all of the characteristics that I was looking for, so I cracked open a brand new widget and pasted in the HTML and the CSS. Then I created a new Portal Page and dragged my new widget into a full-width container and pulled it up to see how it looked. Since it is just a template, everything is hard-coded, but at this point, I just wanted to make sure that I had all of the parts and pieces and that it came out looking as it should (which it did). This is just the parts that I had found at this stage, pasted into a new Service Portal Widget.

Calendar template as a Service Portal Widget

OK, so far, so good. This was the basic structure that I wanted; now, I just needed to replace the hard-coded data with something a little more dynamic. To start with, I wanted to be able to navigate to the next month and to the previous month. Nothing beyond that — just a simple forward/backward capability that would let you move around a bit. So I tinkered with the HTML for the header and came up with this:

<header>
  <span class="col-sm-4">
    <button class="btn btn-default" ng-click="newMonth(-1);"><< Previous Month</button>
  </span>
  <span class="col-sm-4">
    <h1>{{data.monthLabel}}</h1>
  </span>
  <span class="col-sm-4">
    <button class="btn btn-default" ng-click="newMonth(1);">Next Month >></button>
  </span>
</header>

This simply divided the header into three equal parts, the “go back” button, the original title (based on a variable now), and the “go forward” button. I had both buttons call the same routine, passing either a +1 or a -1, depending on which direction you wanted to go. That routine lives in the client script, which now looks like this:

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

	$scope.newMonth = function(offset) {
		var yy = parseInt(c.data.year);
		var mm = parseInt(c.data.month) + offset;
		if (mm < 0) {
			mm = 11;
			yy = yy - 1;
		} else if (mm > 11) {
			mm = 0;
			yy = yy + 1;
		}
		var s = $location.search();
		s.month = mm;
		s.year = yy;
		var newURL = $location.search(s);
		spAriaFocusManager.navigateToLink(newURL.url());
	}
}

The routine simply navigates to the same page with different values for the year and month URL parameters. To process those parameters when the page loads, we also need a little code on the server side:

var monthName = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var today = new Date();
data.month = today.getMonth();
data.year = today.getFullYear();
if ($sp.getParameter('month') && $sp.getParameter('year')) {
    data.month = $sp.getParameter('month');
    data.year = $sp.getParameter('year');
}
data.monthLabel = monthName[data.month] + ' ' + data.year;

This code basically establishes the values of the month and year based on the current date, and then if there is both a month and a year parameter on the URL, it overrides that initial value with the values passed via the URL. Then, using an array of month names, it establishes the calendar heading label based on the month and year values. At this point, we still have a hard-coded calendar body, but we can now use the new buttons to move forward and back through time, and the heading will change, even if the calendar itself still remains constant. All of that appears to work at this point, so now we just have to make the calendar itself as dynamic as the heading value.

To make the HTML portion simpler, I decided to build out the structure of the days to be displayed in the server side script. The structure is simply an array of 4 to 6 weeks, with each week containing an array of 7 days. The number of weeks in a month is variable, depending on how many 7-day rows it will take to display all of the days in the month, but the number of days in a week is always 7, regardless of how many of those days actually fall in the month to be displayed. The code to build out the model array is fairly self-explanatory, so I won’t dwell on that here, but here it is, for those of you interested in the details:

data.week = [];
data.firstDay = new Date(data.year, data.month, 1);
var offset = data.firstDay.getDay();
data.thisDay = new Date(data.firstDay.getTime());
while (data.thisDay.getMonth() == data.month) {
	var thisWeek = [];
	data.week.push(thisWeek);
	for (var i=0; i<7; i++) {
		var thisDay = {};
		thisWeek.push(thisDay);
		if (data.week.length > 1 || i >= offset) {
			if (data.thisDay.getMonth() == data.month) {
				thisDay.date = data.thisDay.getDate();
				data.thisDay.setDate(data.thisDay.getDate() + 1);
			}
		}
	}
}

Building the model in the server-side code makes the HTML portion quite simple. I deleted all of the hard-coded example code (except for the day of the week labels) and then replaced it with this:

<div id="calendar">
  <ul class="weekdays">
    <li>Sunday</li>
    <li>Monday</li>
    <li>Tuesday</li>
    <li>Wednesday</li>
    <li>Thursday</li>
    <li>Friday</li>
    <li>Saturday</li>
  </ul>
  <ul ng-repeat="w in data.week" class="days">
    <li ng-repeat="d in w" class="day" ng-class="{'other-month':!d.date}">
      <div class="date" ng-if="d.date">{{d.date}}</div>
    </li>
  </ul>
</div>

Basically, it is just a couple of ng-repeats, one for the array of weeks and another within that for the 7 days of the week. The original calendar template had numbers for all of the days, whether they were in the current month or not, but I didn’t really like that approach, so I did not calculate those values, and threw in an ng-if to keep that number DIV from appearing for those days that do not belong to the month that is the subject of the current display. At this point, we now have a completely empty calendar based on the selected month and year:

Blank calendar with dynamic days based on active month/year

This is pretty much the empty calendar shell that I had envisioned, although at this point there is no way to pass in content for any given day. I still have to work out the best way to go about that, but conceptually, this is exactly the kind of thing that I was hoping to produce: a plain monthly calendar with moderate navigation capabilities and the potential for adding custom content based on the use case. This looks like a good stopping place for now, so I have bundled up the parts thus far and created an Update Set for anyone who might have an interest. Next time, I will add the capability to pass in content, and come up with some kind of example to demonstrate how that all works.

Portal Widgets on UI Pages, Revisited

“No matter how far down the wrong road you have gone, turn back.”
Turkish Proverb

I love making parts. That’s pretty much what I do. But even more than that, I love finding parts. If I can locate the part that I need, then I don’t have to build it, and even more important, I don’t have to maintain it. Nothing lasts forever, and all parts require periodic maintenance at one point or another, if for no other reason that to keep pace with an ever changing world. Even if I have already created a part for a particular purpose, if I can find a viable replacement, then I will gladly discard my own creation in favor of an acceptable newly released component or third-party alternative.

In fact, it doesn’t even have to be new — ServiceNow is so chock full of valuable gems that it would be impossible for any single individual to know and understand what’s under every rock in every corner of every room. I find stuff all of the time that I had no idea had been in there all along. And when I find an out-of-the-box doodad that can replace some custom-crafted gizmo that I built because I didn’t know any better, I will gladly toss aside my own creation to embrace what the product has to offer.

Not too long ago, I was pretty proud of myself for coming up with a way to display Portal Pages in a modal pop-up on a UI Page. That was pretty cool at the time, but what is even better is to find out that I never had to go to all of the trouble. My way of doing it looked like this:

var dialog = new GlideDialogWindow("portal_page_container");
dialog.setPreference('url', '/path/to/my/widget');
dialog.render();

… and it required a component that I had built for just that purpose, the portal_page_container. That worked, which is always import. However, there is a better way: using a built-in component that will essentially accomplish the same thing without the need for the extra home-made parts. Instead of a GlideDialogWindow, the trick is to use a GlideOverlay:

var go = new GlideOverlay({
	title: 'Title of My Widget',
	iframe : '/path/to/my/widget',
	closeOnEscape : true,
	showClose : true,
	height : "90%",
	width : "90%"
});
go.center();
go.render();

Now I can pitch that portal_page_container into the trash. It served its purpose honorably, but when things are no longer needed, it’s time to let go and move on!

Update Sets

“Good order is the foundation of all things.”
Edmund Burke

I’m starting to get quite the collection of various Update Sets from all of the little parts and pieces that I have been playing around with lately, so I decided to try to get a little organized and create a space for them all in the hopes of making it a little easier to hunt down what you might be looking for. You can see the result here. I also added a link to the header menu bar to make it easier to find.

I collected all of the different versions of each little project under a single title, which also serves as a link to the introductory article on the related subject. It’s not in any particular order, so you still might have to do a little nosing around to find anything specific, but at least everything is in one place now, and you can see all of the multiple versions of things together in one spot. It’s not much for style, but it’s functional, so that’s enough effort invested into that little project for now.

@mentions in the Service Portal, Revisited

“If you don’t have time to do it right, when will you have the time to do it over?”
John Wooden

After adding a mention type to my Service Portal form field tag, I broke my original ment.io example. I actually used that example in building the code to support the mentions type, but I did have to make a few changes in order to get everything to work, and now that I have done that, my original example doesn’t work anymore. I thought about just abandoning it since I got the form field version working, but then I thought that there were some other possible use cases where I wouldn’t want to be using an snh-form-field, so I decided that I had better dig around and see what was what.

The form field tag version did make things quite a bit simpler. Compare the initial version:

<snh-form-field
  snh-model="c.data.textarea"
  snh-name="textarea"
  snh-type="textarea"
  snh-label="${Original 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('textarea', item)"
  mentio-typed-term="typedTerm"
  mentio-id="'textarea'"
  ng-trim="false"
  autocomplete="off"/>

… with the snh-form-field version, and you can see right away that there is a significant difference in size alone:

<snh-form-field
  snh-model="c.data.mention"
  snh-name="mention"
  snh-type="mention"
  snh-label="${snh-form-field ment.io Example}"/>

Just for comparison, I decided to put both on the page (and make them both work!), and then add another using the new feedback field type, and yet another using just a plain, old, ordinary textarea.

new @mentions example page with various versions

The errors on the original weren’t that significant. Mainly I just wanted to put out a new version of the Update Set so that you could see all of the various approaches working on the same page side by side (… well, on top of each other, if you really want to get specific). If you are really curious as to what exactly needed to be changed, you can always compare the old to the new.

Even More Service Portal Form Fields

“Have a bias toward action – let’s see something happen now. You can break that big plan into small steps and take the first step right away.”
Indira Gandhi

After working out all of the little issues in my @mentions example, it occurred to me that it might be even better to just make an @mentions textarea another supported field type like I just did for feedback. This way, you wouldn’t have to bother with all of those mentio- prefixed attributes at all — they would just become part of the internal process of rendering the form field. Some things would have to be standardized, which might limit your flexibility in certain areas, but that still wouldn’t prevent you from mentionizing your own textarea or textarea-type form field. This would just be yet another option.

The first thing to, then, would be to add mention to the list of supported field types:

	var SUPPORTED_TYPE = ['choicelist', 'date', 'datetime-local', 'email', 'feedback', 'inlineradio', 'mention', 'month', 'number', 'password', 'radio', 'rating', 'reference', 'select', 'tel', 'text', 'textarea', 'time', 'url', 'week'];

That’s getting to be quite a list, but that’s not necessarily a bad thing. Next, we have to add the logic to render the input element:

} else if (type == 'mention') {
	htmlText += "      <textarea class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" mentio=\"\" mentio-macros=\"macros\" mentio-trigger-char=\"'@'\" mentio-items=\"members\" mentio-search=\"searchMembersAsync(term)\" mentio-template-url=\"/at-mentions.tpl\" mentio-select=\"selectAtMention('" + name + "', item)\" mentio-typed-term=\"typedTerm\" mentio-id=\"'" + name + "'\" ng-trim=\"false\" autocomplete=\"off\"" + passThroughAttributes(attrs) + (required?' required':'') + "></textarea>\n";

So far, so good. Now, where to stuff the template? In my mind, the best place to locate the template would be in the Service Portal itself, as that would ensure that it would only appear on the page once and only once. You could just make it part of your portal page header and be done with it. However, if you didn’t know to do that, or if you moved your widget from a portal that had it to another portal that did not, things would not work and would not be that intuitive as to why that was. Placing it in the widget is another option, but again, you would have to know to do that. Rendering it as part of the form field means that you could have many copies of the template ending up on a single page, but then everything would be self-contained and you wouldn’t have to know to add anything else to include this feature other than the appropriate form field type. Right now, I am thinking that this latter approach is the best option, although I may end up going the other way one day after I’ve had more time to contemplate all of the ramifications of that strategy. For now, I’ve added this line just under the lines above:

	htmlText += getMentionTemplate();

… and this function to provide the template:

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

That does create the potential for more than one template appearing in the source code, but other than the obvious bloat, that doesn’t really hurt anything.

Now we have to deal with the functions, one to fetch the data and one to handle the selection. We will add these to the link section of the provider, starting with the one that fetches the data:

scope.searchMembersAsync = function(term) {
	scope.userSysId = window.NOW.user_id;
	scope.members = [];
	scope.members.loading = true;
	clearTimeout(scope.typingTimer);
	if (term.length === 0) {
		scope.members = [{
			termLengthIsZero: true
		}];
		scope.members.loading = false;
	} else {
		scope.typingTimer = setTimeout(function() {
			scope.snMention.retrieveMembers('sys_id', scope.userSysId, term).then(function(members) {
				scope.members = members;
				scope.members.loading = false;
			}, function () {
				scope.members = [{
					termLengthIsZero: true
				}];
				scope.members.loading = false;
			});
		}, 500);
	}
};

This script required two things that we brought in via the client script function arguments: snMention and $timeout. In this context, $timeout can be replaced with the standard window functions setTimeout and clearTimeout, so that eliminates the need for that one. The other requirement, though, snMention, is still going to have to come in via a client script function argument, and then passed into the scope. I don’t like doing that, as that is yet another thing that the person using this is going to have to know, but I couldn’t see any other way around it. That makes the client script in our example widget now start out like this:

function($scope, snMention) {
	var c = this;
	$scope.snMention = snMention;

The other script we will need is the one that handles the selection:

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

The major revision here is that we have added a field argument to the function call to allow for the fact that there could be more than one field on the form that contains @mentions. This way, mentions from one field are collected in a different object than mentions from another field. We also added checks for both the base mention map and the field-specific map so that they can be initialized as needed.

To test things out, I added a mention type form field to my form field test widget and gave it a whirl.

Form field test page with @mention field added

Everything seems to work, which is good, so I’ve bundled it all up into yet another Update Set just in case someone wants to dig into all of the details. I’m going to have to quit using the word final in relationship to this little form field experiment, as I keep finding new form field types to toss onto the pile; who knows what will pop up tomorrow …

@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.

More Service Portal Form Fields

“Code reuse is the Holy Grail of Software Engineering.”
Douglas Crockford

One of the thing that I like about making parts is that, even after you’ve “completed” your work and put a part on the shelf, you can always go back at some point later on and make it even better. When I first set out to create my form field tag, my primary goal was to save myself some work and to set things up so that things would always come out consistently. Consistency is a nice by-product of reusing the same component over and over again. People like it when things are consistent. So when I came across a need for a field type that I had not built into the current version of my form field tag, my first impulse was to pull it off the shelf, dust it off, and give it a bit of an upgrade.

What I needed was a feedback field, which is really a combination to two separate fields, a rating widget and a comments box. There are all kinds of rating widgets out there where you can configure stars or happy faces or some other graphic to indicate some level of satisfaction, but none of the existing field types in my current form field implementation supported that feature, and none of them included a single label for two input elements. What I wanted to do was to support something like this:

Example feedback entry

For the rating, I peeked under the hood of the Knowledge Article widget, and found the uib-rating tag, which looks like it comes from here. That looked like just the thing that I needed, so I all that was left for me to do was to wrap my labels and decorations around that widget in the same manner as I had for the sn-record-picker and sn-choice-list tags. The code for the stand-alone rating was pretty much just a copy, paste, and slightly modify:

if (type == 'radio' || type == 'inlineradio') {
	htmlText += buildRadioTypes(attrs, name, model, required, type);
} else if (type == 'select') {
	htmlText += buildSelect(attrs, name, model, required);
} else if (SPECIAL_TYPE[type]) {
	htmlText += buildSpecialTypes(attrs, name, model, required, type, fullName, label);
} 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 == 'choicelist') {
	htmlText += "      <sn-choice-list sn-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></sn-choice-list>\n";
} else if (type == 'rating') {
	htmlText += "      <uib-rating ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></uib-rating>\n";
} else ...

For the combination of a rating and a comment block, things got a little more complicated. In all of my other field types, I passed through to the input element all of the original attributes that did not have some other purpose in rendering out the entire block of code. For the first time, I had more than one input element, as I was combining the rating doodad with the textarea for the comments all under one label. After experimenting with different ways to distinguish attributes for one of the elements from attributes for the other, I decided against making things more complicated than they needed to be, and just assume that all non-standard attributes would be attributed to the textarea. Once that was settled, the resulting code turned to be the following:

...
} else if (type == 'rating') {
	htmlText += "      <uib-rating ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\"" + passThroughAttributes(attrs) + (required?' required':'') + "></uib-rating>\n";
} else if (type == 'textarea' || type == 'feedback') {
	if (type == 'feedback' && attrs.snhRatingModel) {
		var max = 5;
		if (attrs.snhRatingMax && parseInt(attrs.snhRatingMax) > 0) {
			max = parseInt(attrs.snhRatingMax);
		}
		htmlText += "      <uib-rating ng-model=\"" + attrs.snhRatingModel + "\" max=\"" + max + "\"/>\n";
	}
	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";
}

As you can see, I did break down and allow for the max attribute by creating an snh-rating-max attribute that would not be passed in to the textarea, but other than that, the rating paired with the comments ends up basically unconfigurable. I may change that one day, but for now, this was all I needed to get me what I was after.

Anyway, I have done a little testing, and it all seems to work so far. If you would like to play around with it on your own, here is an Update Set with all of the relevant parts and pieces.