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

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

More Fun with Highcharts

“Never give up on something that you can’t go a day without thinking about.”
Winston Churchill

Those of us who develop software for a living always like to blame the customer for the inevitable scope creep that works its way into an assignment or project. The real truth of the matter, though, is that a lot of that comes right from the developers themselves. Often, just when you think you are about to wrap something up and call it complete, you get that nagging you-know-it-would-be-even-better-if-we-added-this feeling that just won’t go away.

It was my intention to make my previous installment on the Highcharts Workload Chart my last and final offering on the subject. Wait … this sounds way too familiar. OK, fine … I have a bad habit of continuing to tinker with stuff long after it should have been put to bed. But, I did learn something new this time, so I think it was worth the return trip. All I really wanted to do was to add a couple more relevant charts to my workload status page:

Work Distribution and Aging Charts

I already had the Generic Chart widget in hand, so I just needed to drop it on the page a couple more times and run a few more queries to fetch the relevant data. How hard could it be? Well, experience gives us the answer to that question! As usual, things didn’t go quite as smoothly as I had anticipated. It turns out that the Generic Chart widget contains a fatal flaw that only allows it to be used once per page. In the HTML for the widget, I hard-coded the ID of the DIV that will contain the chart, which you have to pass to Highcharts so that the chart will be rendered in that specific DIV. Well, when you put more than one Generic Chart widget on a page, they all want to render their charts in the first instance of a DIV with that ID. That’s not going to work!

The fix wasn’t too bad, but it did require a fix. The revised HTML now looks like this:

<div class="panel panel-default">
  <div class="panel-body form-horizontal">
    <div class="col-sm-12">
      <div id="{{data.container}}" style="height: 400px;"></div>
    </div>
  </div>
</div>

To populate the new data.container variable, I set up yet another widget option, and then set up a default value for the option so that you would only need to mess with this if you were working on a multi-chart page. That, and the addition of the code to populate the two other charts, one a Pie Chart and one a Bar Chart, were really the only other additions. If you want to take a look at the whole thing in action, here is the most recent Update Set.

Fun with Highcharts, Part VII

“Time, which alone makes the reputation of men, ends by making their defects respectable.”
Voltaire

Once again, things were not quite as bad as I had originally envisioned. I definitely disliked returning to my workload chart page and having it revert back to all of the original, default settings. This was not good. But how best to fix it? I didn’t have that problem with my Data Table Content Selector, which also has multiple options, but that was built using a little bit different approach. Although there are two independent widgets on the page, they don’t really communicate with one another. The selector on the current page communicates with the Data Table widget on the next page, passing all of the information via URL parameters when it builds the URL for the next page. Reconfiguring the workload chart widget and underlying generic chart widget to adopt this philosophy seemed like a lot more work that I really wanted take on right at the moment.

Instead, I decided to leverage the same hack that I used in my dynamic breadcrumbs widget, leveraging the User Preferences feature to preserve the selections and then picking them back up each time the page loads. Here is the additional code I added to save the user’s selections in a User Preference that I called workload_selections:

var selections = {};
selections.group = data.group;
selections.type = data.type;
selections.frequency = data.frequency;
selections.ending = data.ending;
gs.getUser().setPreference('workload_selections', JSON.stringify(selections));

User Preferences are strings, so you have to convert the object to a string before you save it, and convert it back to an object when you fetch it back. Here is the code that I used to retrieve it once the page loads again:

selections = gs.getPreference('workload_selections');
if (selections) {
	selections = JSON.parse(selections);
	if (selections.group) {
		data.group = selections.group;
		data.type = getDefaultType();
		data.type = selections.type;
		data.frequency = selections.frequency;
		data.ending = selections.ending;
	}
}

You might have noticed that the value for the type selection was set twice, once to the default value and then again to the value saved in the User Preference. That’s because the getDefaultType function is actually a dual purpose function; not only does it return the default type value, but before it does so, it sets up the type options that are appropriate for the selected group (different groups are associated with different types of tasks). Since the group value preserved may be different than the default value, the appropriate list of type choices needs to be generated for that group before the preserved type value is established. Other than that, it’s pretty straightforward stuff.

That resolved the last known defect that I have been able to find so far, so it’s finally time to release another Update Set. If you happen to pull it down and try to use it, let me know if you find any others … thanks!

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

Fun with Highcharts, Part VI

“If you’re going through Hell, keep going.”
Someone other than Winston Churchill

Well, it turns out that it wasn’t as bad as I had originally imagined. I converted my server side GenericChartUtil Script Include into a client side UI Script, then created a Widget Dependency referencing the UI Script, and then finally associated the Widget Dependency to my Generic Chart widget. That pushed the chart object generation from the server side to client side (where I would no longer lose any functions in the chart object), but to make it all work, I needed to pass the chart data and chart type around rather than the completed chart object.

On my Generic Chart widget, I removed the chartObject option and replaced it with two new widget options, chartType and chartData. Then I added a line of code in the client side script to pass the chartData and chartType to the new client side UI Script to generate the chartObject. The client side code for the widget now looks like this:

function($scope, $rootScope, $location) {
	var c = this;
	if (c.data.chartData) {
		c.data.chartData.location = $location;
		$scope.chartOptions = genericChartUtil.getChartObject(c.data.chartData, c.data.chartType);
	}
	if (c.options.listen_for) {
		$rootScope.$on(c.options.listen_for, function (event, config) {
			if (config.chartData) {
				config.chartData.location = $location;
				$scope.chartOptions = genericChartUtil.getChartObject(config.chartData, config.chartType);
			}
		});
	}
}

On my Workload Chart widget, I removed all references to the deleted Script Include on the server side, and then modified the broadcast message on the client side to pass the chartData and chartType rather than the entire generated chartObject. That code now looks like this:

function($scope, $rootScope) {
	var c = this;
	$scope.updateChart = function() {
		c.server.update().then(function(response) {
			c.data.config = response.config;
			c.data.group = response.group;
			c.data.type = response.type;
			c.data.frequency = response.frequency;
			c.data.ending = response.ending;
			c.data.chartData = response.chartData;
			$rootScope.$broadcast('refresh-workload', {chartData: c.data.chartData, chartType: 'workload'});
		});
	}
}

That solved my earlier problem of losing the functions built into the chart objects that were generated on the server side. Now I could get back to what I was trying to do in the first place, which was to set things up so that you could click on any given data point on the chart and pull up a list of the records that were represented by that value. This code was now working as it should:

plotOptions: {
        series: {
            cursor: 'pointer',
            point: {
                events: {
                    click: function () {
                        alert('Category: ' + this.category + ', value: ' + this.y);
                    }
                }
            }
        }
    },

For my link URL to show the list of records represented by the chart item clicked, I was going to need the name of the table and the filter used in the GlideAggregate that calculated the value. Neither one of those was currently passed in the chart data, so the first thing that I needed to do was to modify the code that generated the chartData object to include those values. That code now looks like this:

function gatherChartData() {
	var task = new GlideAggregate(data.type);
	var periodData = getPeriodData();
	var chartData = {};
	chartData.table = data.type;
	chartData.filter = {Received: [], Completed: [], Backlog: []};
	var filter = '';
	chartData.title = task.getPlural() + ' assigned to ' + findOption(data.config.groupOptions, data.group).label;
	chartData.subtitle = periodData.frequencyInfo.label + ' through ' + periodData.endingDateInfo.label;
	chartData.labels = periodData.labels;
	chartData.received = [];
	chartData.completed = [];
	chartData.backlog = [];
	for (var i=1; i<periodData.endDate.length; i++) {
		// received
		filter = 'assignment_group=' + data.group + '^opened_at>' + periodData.endDate[i-1] + '^opened_at<=' + periodData.endDate[i];
		task.initialize();
		task.addAggregate('COUNT');
		task.addEncodedQuery(filter);
		task.query();
		task.next();
		chartData.received.push(task.getAggregate('COUNT') * 1);
		chartData.filter.Received.push(filter);
		// completed
		filter = 'assignment_group=' + data.group + '^closed_at>' + periodData.endDate[i-1] + '^closed_at<=' + periodData.endDate[i];
		task.initialize();
		task.addAggregate('COUNT');
		task.addEncodedQuery(filter);
		task.query();
		task.next();
		chartData.completed.push(task.getAggregate('COUNT') * 1);
		chartData.filter.Completed.push(filter);
		// backlog
		filter = 'assignment_group=' + data.group + '^opened_at<=' + periodData.endDate[i] + '^closed_at>' + periodData.endDate[i] + '^ORclosed_atISEMPTY';
		task.initialize();
		task.addAggregate('COUNT');
		task.addEncodedQuery(filter);
		task.query();
		task.next();
		chartData.backlog.push(task.getAggregate('COUNT') * 1);
		chartData.filter.Backlog.push(filter);
	}
	return chartData;
}

Since I need the filter value for multiple purposes, I first assigned that to a variable, and then used that variable wherever it was needed. Inside of Highcharts, my only reference to the series was the category name, which is why there is an odd capitalized property key for the chartData.filter properties (the category names are labels on the chart, so they are capitalized). These changes gave me the data to work with so that I could modify the onclick function to look like this:

plotOptions: {
	series: {
		cursor: 'pointer',
		point: {
			events: {
				click: function (evt) {
					var s = {id: 'snh_list', table: chartData.table, filter: chartData.filter[this.series.name][this.index]};
					var newURL = chartData.location.search(s);
					spAriaFocusManager.navigateToLink(newURL.url());
				}
			}
		}
	}
},

In order for that to work, I need to pass the $location object to Highcharts as well, so my client side GenericChart widget code ended up looking like this:

function($scope, $rootScope, $location) {
	var c = this;
	if (c.data.chartData) {
		c.data.chartData.location = $location;
		$scope.chartOptions = genericChartUtil.getChartObject(c.data.chartData, c.data.chartType);
	}
	if (c.options.listen_for) {
		$rootScope.$on(c.options.listen_for, function (event, config) {
			if (config.chartData) {
				config.chartData.location = $location;
				$scope.chartOptions = genericChartUtil.getChartObject(config.chartData, config.chartType);
			}
		});
	}
}

To get back to the chart itself, once you clicked on a data point to see the underlying records, I added my dynamic breadcrumbs widget to the the top of the chart page and to the top of a new list page that I created for this purpose. Now it was time to test things out …

Well, as often seems to be the case with these things, there is good news and bad news. The good news is that clicking on the data points on the chart actually does bring up the list of records, which is very cool. Even though I had to do a considerable amount of restructuring of my initial concept, everything now seems to work. And I like the feature. Clicking on a bar or point on the line now takes you to a list of the records that make up the value for that data point. And when you are done, you can click on the breadcrumb for the chart and get back to the chart itself. All of that works beautifully.

Unfortunately, when you get back to the chart page, it reverts to the original default settings for all of the options. If you used any of the four selections at the top of the page to get to a specific chart configuration, and then clicked on a data point to see the underlying records, when you returned to the chart, you were no longer looking at the chart that you had selected. You were back to the original, default values for all of the selections. The page basically restarts from the beginning. That, I do not like at all.

The primary reason for that behavior is that your chart option selections are not part of the URL, so they are not preserved when you click on the breadcrumb to return. Making them part of the URL would mean another complete reconfiguration of the way in which that chart works, but something is going to have to be done. I don’t like it the way that it works now. Originally, I was going to release a new Update Set with all of these changes, but I’m not happy with the way things are working right now. I’m going to have to do a little bit more work before I’m ready to release another version. Hopefully, I can do that next time out.