Companion Widgets for SNH Data Tables

“Few things are harder to put up with than the annoyance of a good example.”
Mark Twain

A while back we pushed our SNH Data Table Widgets out to Share, and then later made a few revisions and enhancements to come up with the version that is posted out there now. After spending a little time refactoring and standardizing some of the processes, we now have a fairly consistent way of handling reference links, bulk actions, buttons, icons, and the new scripted value columns. In most cases, there is a $rootScope broadcast message that just needs to be picked up by a companion widget, and then the companion widget handles all of the custom stuff that won’t be found in the common components. Most of the work that we did during that development process was centered on the shared components; the few companion widgets that we did throw in as samples were not the focus of the discussion. Now that the development of the table widgets is behind us, it is a good time to spend a little more time on the companion widgets and how they augment the primary artifacts.

Let’s say that we have a policy that Level 2 and 3 technicians working Incidents should respond to all customer comments, and even without customer inquiries, no open ticket should ever go more than X number of days without some comment from the assigned technician indicating the current status. To assist the technician in managing that expectation, we could configure a list of assigned incidents that included some visual indication of the comment status of each.

List of assigned Incidents with current comment status

For those tickets where a comment is needed, we would want to provide a means for the technician to provide that comment right from the list. One way to do that would be to pop up the Ticket Conversations widget in a modal dialog. This would allow the tech to both view the current conversion and add their own comments.

Ticket Conversations widget pop-up

Once a comment has been provided, the comment appears in the conversation stream, after which additional comments can be provided or the pop-up window closed.

New comment posted in the Ticket Conversations widget

Once the modal pop-up window is closed, the list should be updated to reflect the new comment status of the incident.

Updated Incident list after providing required comment

Here is the configuration script to produce the above table using the SNH Data Table from JSON Configuration widget.

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

	perspective: [{
		name: 'all',
		label: 'All',
		roles: ''
	}],

	state: [{
		name: 'all',
		label: 'All'
	}],

	table: {
		all: [{
			name: 'incident',
			displayName: 'Incident',
			all: {
				filter: 'active=true^assigned_toDYNAMIC90d1921e5f510100a9ad2572f2b477fe',
				fields: 'number,caller_id,state,opened_at,short_description',
				svcarray: [{
					name: 'comment',
					label: 'Comment',
					heading: '',
					script: 'global.LastCommentValueProvider'
				}],
				aggarray: [],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			}
		}]
	},

	type: 'MyWorkConfig'
});

The Data Table widget can be configured to produce the list, but to launch the modal pop-up, you will need to add a companion widget to the page. A companion widget shares the page with a Data Table widget, but has no visual component. It’s job is simply to listen for the broadcast messages and take whatever action is desired when a message of interest is received. At the top of the Client script of all companion widgets, I like to throw in this little chunk of code to help identify the values used to distinguish each potential message.

var eventNames = {
	referenceClick: 'data_table.referenceClick',
	aggregateClick: 'data_table.aggregateClick',
	buttonClick: 'data_table.buttonClick',
	bulkAction: 'data_table.bulkAction'
};

In this particular case, we are looking for a button click, and since there are no other buttons configured in this instance, we don’t even need to check to see which button it was.

$rootScope.$on(eventNames.buttonClick, function(e, parms) {
	var modelOptions = {
		title: "${Incident Conversation}",
		widget: "widget-ticket-conversation",
		widgetInput: {
			sys_id: parms.sys_id,
			table: 'incident'
		},
		buttons: [
			{label: '${Done}', cancel: true}
		],
		size: 'lg'
	};
	spModal.open(modelOptions).then(function() {
		$window.location.reload();
	}, function() {
		$window.location.reload();
	});
});

Basically, this is just your typical spModal widget open, passing in some widget input. In the case of the Ticket Conversations widget, you need both a table name and the sys_id of a record on that table. In our case, we know that the table is the Incident table, and we can obtain the sys_id of the incident in the row from the parameters passed in with the broadcast message. When the modal window is closed, there are two functions passed in as arguments to the then function, the first for a successful completion and the second for a cancellation. In our case, we want to reload the page for either result, so the logic for each is the same.

For most circumstances, this would be the extent of the widget. For the most part, companion widgets are pretty simple, narrowly focused components that are built for a single purpose: to accomplish something that is not built in to the standard artifacts. Everything else is left for the primary widgets. We couldn’t get away without throwing in a little bit of hackery, though, since that’s what we do around here, so in this particular example, we will need to add just a bit more to the widget before we can call it complete.

In our example, we are using the class of the button to visually identify the current state of the comments on each incident. This cannot be done with the standard button configuration, as the class name is one of the configuration properties, and it is applied to all buttons in the column. To produce specific class values for each row, we have to resort to using a scripted value column instead of configuring a button. Each scripted value column requires a script to produce the value, and for this particular example, our script looks like this:

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

	getScriptedValue: function(item, config) {
		var className = '';
		var helpText = '';

		var journalGR = this.getLastJournalEntry(item.sys_id);
		if (journalGR.isValidRecord()) {
			if (journalGR.getValue('sys_created_by') == gs.getUserName()) {
				if (journalGR.getValue('sys_created_on') > gs.daysAgo(7)) {
					className = 'success';
					helpText = 'Click here to add additional comments';
				} else {
					className = 'danger';
					helpText = 'Click here to update the status';
				}
			} else {
				className = 'danger';
					helpText = 'Click here to respond to the latest comment';
			}
		} else {
			className = 'default';
					helpText = 'Click here to provide a status comment';
		}

		return this.getHTML(item, className, helpText);
	},

	getHTML: function(item, className, helpText) {
		var response = '<a href="javascript:void(0)" role="button" class="btn-ref btn btn-';
		response += className;
		response += '" onClick="tempFunction(this, \'';
		response += item.sys_id;
		response += '\')" title="';
		response += helpText;
		response += '" data-original-title="';
		response += helpText;
		response += '">Comment</a>';
		return response;
	},

	getLastJournalEntry: function(sys_id) {
		var journalGR = new GlideRecord('sys_journal_field');
		journalGR.orderByDesc('sys_created_on');
		journalGR.addQuery('element_id', sys_id);
		journalGR.setLimit(1);
		journalGR.query();
		journalGR.next();
		return journalGR;
	},

	type: 'LastCommentValueProvider'
};

Basically, we go grab the last comment, and look at the author and the date to determine both the class name and the associated help text for the button. Once that has been determined, then we build the HTML to produce the button in a similar fashion to the buttons created using the button/icon configuration. Those of you paying close attention will notice that the one significant difference between this HTML and the HTML produced from a button/icon configuration is the use of onClick in the place of ng-click. This has to be done because the HTML added to the page for a scripted value column is not compiled, so an ng-click will not work. The problem with an onClick, though, is that it is outside the scope of the widget, so we have to add this little tidbit of script to the HTML of our companion widget to address that.

<div>
<script>
function tempFunction(elem, sys_id) {
 var scope = angular.element(elem).scope();
  scope.$apply(function() {
    scope.buttonClick('comment', {sys_id: sys_id});
 });
}
</script>
</div>

This brings things full circle and gets us back inside the scope of the primary widget to activate the normal button click process, which will send out the $rootScope broadcast message, which will in turn get picked up by our companion widget. Normally, the HTML for a companion widget would be completely empty, but in this particular case, we were able to leverage that section to insert our little client script. I plan to bundle all of these artifacts up into an Update Set so that folks can play around with them, but before I do that, I wanted to throw out a couple more examples. We will take a look at another one of those next time out.

Scripted Value Columns, Enhanced, Part II

“Alone we can do so little; together we can do so much.”
Helen Keller

Last time, we tinkered with the Scripted Value Columns section of the SNH Data Table Widgets, and created an example Task list using an aggregation of labor hours as a demonstration of how you can include HTML with your returned value. Today we will have a look at another scripted column in that example, the Customer column.

There are quite a number of extensions to the core Task table in ServiceNow, all of which share the same core list of fields. One of those fields is called opened_by, and identifies the person who was signed on to the system when the task was initially opened. That person is not necessarily the customer, though. In the case of an Incident, for example, that is more likely to be a call center agent who took the call, not the person who is reporting the issue. There is no core table field to represent the customer. Each extension of the Task table has its own way of identifying the person awaiting the completion of the task. Using the scripted value column feature, though, you can create a “customer” column by looking at the type of task, and then grabbing the value of the field that is appropriate for that particular type.

getScriptedValue: function(item, config) {
	var response = '';

	var table = item.sys_class_name.value;
	var field = 'opened_by';
	if (table == 'incident') {
		field = 'caller_id';
	} else if (table == 'sc_request') {
		field = 'requested_for';
	} else if (table == 'sc_req_item') {
		field = 'request.requested_for';
	} else if (table == 'sc_task') {
		field = 'request_item.request.requested_for';
	} else if (table == 'change_request') {
		field = 'requested_by';
	}
	var taskGR = new GlideRecord(table);
	if (taskGR.get(item.sys_id)) {
		if (taskGR.getValue(field) > '') {
			response = taskGR.getDisplayValue(field);
		}
	}

	return response;
}

This particular example handles incidents, catalog activity, and change requests, but could easily be modified to handle any number of other types of extensions to the core task table. This just returns the value, though. If you wanted to make the field a link to the user profile page, for example, you could do something like this.

getScriptedValue: function(item, config) {
	var response = '';

	var table = item.sys_class_name.value;
	var field = 'opened_by';
	if (table == 'incident') {
		field = 'caller_id';
	} else if (table == 'sc_request') {
		field = 'requested_for';
	} else if (table == 'sc_req_item') {
		field = 'request.requested_for';
	} else if (table == 'sc_task') {
		field = 'request_item.request.requested_for';
	} else if (table == 'change_request') {
		field = 'requested_by';
	}
	var taskGR = new GlideRecord(table);
	if (taskGR.get(item.sys_id)) {
		if (taskGR.getValue(field) > '') {
			var sysId = taskGR.getValue(field);
			var name = taskGR.getDisplayValue(field);
			response += '<a href="?id=user_profile&table=sys_user&sys_id=';
			response += sysId;
			response += '" aria-label="${Click for more on }';
			response += name;
			response += '">';
			response += name;
			response += '</a>\n';
		}
	}

	return response;
}

That would provide an output such as this.

Customer column as link to user profile page

The one thing that the Customer column does not have that the Assigned to column, which is a standard SNH Data Table user reference, does have is the user avatar image. That’s easily done with the sn-avatar tag, but that tag is not simple HTML, and not processed using the ng-bind-html attribute. Theoretically, it can be processed using the sc-bind-html-compile attribute, but I have never had any luck in utilizing this component. I tried it, and couldn’t get it to work, so I gave up and went back to using the ng-bind-html attribute. So, there is no user avatar in this version. Maybe one day I will figure that out, but that day is not today.

Speaking of figuring things out one day, I still have not been able to get rid of that extra line that appears when there is an image available for the avatar. If you look at the example above, the lines with an avatar image are wider than the lines where that field is blank, or the initials are used in place of an image. This because there is an extra carriage return in there that expands the height of the line. Only when there is an actual image does this occur, but it shouldn’t occur at all. If there is some grand master CSS wizard out there who understands why this is happening and can explain it to me, or better yet, tell me what I can do to prevent it, I would be forever in your debt. I have not been able to figure that one out.

One other thing that you may have noticed in that image is that I got rid of the 0.00 value for tasks that had no hours charged to them. I like that better, and that was a pretty simple thing to do. I still want to have some kind of mouse-over display of who logged the hours, so maybe we will take a look at that next time out.

Conditional Buttons and Icons on the SNH Data Table Widget

“Become a beacon of enhancement, and then, when the night is gray, all of the boats will move towards you, bringing their bountiful riches.”
James Altucher

After I posted the SNH Data Table widget collection out on Share, there were a couple of things that I felt were missing in the version that I put out there. One of those was the ability to click on an aggregate column and pull up a list of the records represented by that value. I took care of that one here, but after having second thoughts, the version that I ended up with is not quite like the one described in that installment of the series. Still, after a litter refactoring, I got it working in a way that finally seemed to be acceptable. But there is still one more thing that I would like to add before I release another version of the collection: the ability to control the presence of a button or icon on the row based on some condition.

Right now, if you configure a button or icon, that button or icon appears on every row of the table. That’s a nice feature that the stock Data Table widget does not include, but it would be even nicer if you could control whether or not the button appeared based on some condition. My thought was that I could add yet one more property to the button/icon configuration object called condition that could be used to control the presence of the action item on the row. I wasn’t exactly sure how to make that happen, but as usual, I thought that I would tackle the easiest portion first, which would be to modify the Content Selector Configurator widget to include that additional property. We just went through that exercise when adding the action property to the aggregate column configuration object (which was later replaced with the hint and page_id properties during refactoring), so the exercise is virtually the same.

As we did with the new aggregate column configuration property, we can start with the main widget and add the following to the list of column headings for the buttons and icons section of the table specification:

<th style="text-align: center;">${Condition}</th>

And then in the repeating rows of that table, we can insert this line:

<td data-th="${Condition}">{{btn.condition}}<

On the client side, we can add the following line to the new record section of the editButton() function:

shared.condition = button.condition;

… and this line to the section that saves the edits in that same function:

button.condition = shared.condition || '';

Finally, on the server side, in the Save() function that rebuilds the script, let’s add these lines in the button/icon specification section:

script += "',\n                 condition: '";
script += thisButton.condition;

That will handle things in most cases, but since this particular property is a Javascript expression, we need to account for the fact that the value might contain single quotes, and surrounding a value containing single quotes with single quotes will result in a syntax error in our Script Include. We should at least check for that, and if single quotes are present in the value, we should escape them. Instead of the script fragment above, which has been working for us in all other cases, let’s expand that a little bit to accommodate our concerns to something like this:

script += "',\n					condition: '";
if (thisButton.condition) {
	var condition = thisButton.condition;
	if (condition.indexOf("'") != -1) {
		condition = condition.replace(/'/g, "\\'");
	}
	script += condition;
}

That takes care of the main widget, but we also need to add a new input field to the pop-up editor before this will actually work, so we need to add this line to the HTML of the Button/Icon Editor widget:

<snh-form-field snh-model="c.widget.options.shared.condition" snh-name="condition"/>

That updates the editor to now include a new button/icon specification property called condition. Of course, that doesn’t actually add any functionality to the Data Table widgets just yet, but at least now we can add a value to that property through the editor, which is a start. Now let’s take a look at the actual SNH Data Table widget and see what we need to do in order to leverage that new property.

Once again, the easiest place to start is with the HTML. Here is the current section of the HTML that deals with buttons and icons:

<td ng-repeat="button in data.btnarray" role="cell" class="text-nowrap center" ng-class="{selected: item.selected}" tabindex="0">
  <a ng-if="!button.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonClick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">{{button.label}}</a>
  <a ng-if="button.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonClick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">
    <span class="icon icon-{{button.icon}}" aria-hidden="true"></span>
    <span class="sr-only">{{button.hint}}</span>
  </a>
</td>

In the current version, there are two mutually exclusive anchor tags controlled by ng-if attributes that look to see whether or not an icon image was specified. We should be able to logically and our new condition to the existing conditions without disturbing the rest of the existing structure. The easiest way to do that at this point would simply be to call a function that dealt with the condition and have it return true or false based on the contents of our new condition property. We will have to build that function, but for now, we can just assume that it exists and modify the above to now look like this:

<td ng-repeat="button in data.btnarray" role="cell" class="text-nowrap center" ng-class="{selected: item.selected}" tabindex="0">
  <a ng-if="!button.icon && buttonCondition(button.condition, item)" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonClick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">{{button.label}}</a>
  <a ng-if="button.icon && buttonCondition(button.condition, item)" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonClick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">
    <span class="icon icon-{{button.icon}}" aria-hidden="true"></span>
    <span class="sr-only">{{button.hint}}</span>
  </a>
</td>

That should take care of the HTML. Now we need to come up with a function that will do what we want to do, which is to return true if we want the button/icon to appear and false if we do not. We can start out with something like this:

$scope.buttonCondition = function(expression, item) {
	var response = true;
	if (expression) {
		// check to see if the condition is true or false
	}
	return response;
};

This defaults the response to true, and if there is no condition specified, then the response will be true. If there is a condition, then we need to see if that condition is true for this item. Basically, we want to run the code that is stored in the property value. For that, we can use $scope.$eval. This AngularJS function will run the expression and return the result, which will then become our response. That will make our function look like this:

$scope.buttonCondition = function(expression, item) {
	var response = true;
	if (expression) {
		response = $scope.$eval(expression, this);
	}
	return response;
};

And that should be that! Now all we need to do is test all of this out, plus do a whole lot of regression testing for all of the refactoring, and then we can finally put out a new Update Set. That is definitely quite a bit of work in an of itself, so let’s save all of that for a future installment.

Aggregate List Columns, Part V

“The trouble you’re expecting never happens; it’s always something that sneaks up the other way.”
George R. Stewart

Last time, we took a little side trip on our journey to add this new functionality to the SNH Data Table Widget collection, but now we need to get back on track and finish up modifying the other wrapper widgets that still need to have some changes. Let’s start with the SNH Data Table from Instance Definition, which does not rely on an outside configuration object. When you configure this widget, you use the Edit feature of the Service Portal Page Designer. The edit dialog box that comes up can be customized for a widget using the Option schema field on the widget form, which already contains a number of customizations for the existing version of the widget. Here is how that looks right now:

[{"hint":"If enabled, show the list filter in the breadcrumbs of the data table",
"name":"enable_filter",
"default_value":"false",
"section":"Behavior",
"label":"Enable Filter",
"type":"boolean"},
{"hint":"A JSON object containing the specification for row-level buttons and action icons",
"name":"buttons",
"default_value":"",
"section":"Behavior",
"label":"Buttons",
"type":"String"},
{"hint":"A JSON object containing the page id for any reference column links",
"name":"refpage",
"default_value":"",
"section":"Behavior",
"label":"Reference Pages",
"type":"String"}]

In the current version, there is a string field for entering a JSON specification object for both Buttons/Icons and Reference Pages, so we will want to add one more just like that for our new aggregate columns. We can clean up the labels for all three while we are at it, just to make things a little clearer, so now it looks like this:

[{"hint":"If enabled, show the list filter in the breadcrumbs of the data table",
"name":"enable_filter",
"default_value":"false",
"section":"Behavior",
"label":"Enable Filter",
"type":"boolean"},
{"hint":"A JSON object containing the specifications for aggregate data columns",
"name":"aggregates",
"default_value":"",
"section":"Behavior",
"label":"Aggregate Column Specifications (JSON)",
"type":"String"},
{"hint":"A JSON object containing the specifications for row-level buttons and action icons",
"name":"buttons",
"default_value":"",
"section":"Behavior",
"label":"Button/Icon Specifications (JSON)",
"type":"String"},
{"hint":"A JSON object containing the page id for any reference column links",
"name":"refpage",
"default_value":"",
"section":"Behavior",
"label":"Reference Page Specifications (JSON)",
"type":"String"}]

Looking over the rest of the code, it doesn’t look as if there is anything else that needs to be done, so the next thing that we need to do is to set up a test so that we can try it out. Once again, we can clone the original test page and then make our modifications to the cloned page. The first thing that we will need to do is to swap out the SNH Data Table from JSON Configuration widget with our modified SNH Data Table from Instance Definition widget. Once we do that, we can click on the Edit pencil in the Page Designer and fill out the modified form.

Top half of the Page Designer widget option editor

For this test, I decided to use the Group table instead of the User table, and for our little test, I decided to limit the list to just ITIL groups. The type field can be more than one type, though, so I had to use a CONTAINS filter rather than an = filter, and the values are sys_ids, not names, so I had to look up the sys_id for the ITIL type. I just added a couple of fields, Name and Manager, and then ordered the list by Name. That pretty much took care of the top half of the widget option editor, so then I scrolled down to the bottom half, where our customizations appear.

Bottom half of the Page Designer widget option editor

Here I left the default color, selected an appropriate glyph icon for a group, and left the Link to this page and Enable Filter fields intentionally blank. In the Aggregate Column Specifications field, I entered a JSON String defining three different aggregate columns:

[{
 "label": "Members",
 "name": "members",
 "heading": "Members",
 "table": "sys_user_grmember",
 "field": "group",
 "filter": "user.active=true"
},{
 "label": "Incidents",
 "name": "incidents",
 "heading": "Incidents",
 "table": "incident",
 "field": "assignment_group",
 "filter": "active=true"
},{
 "label": "Catalog Tasks",
 "name": "sc_tasks",
 "heading": "Catalog Tasks",
 "table": "sc_task",
 "field": "assignment_group",
 "filter": "active=true"
}]

I did not define any buttons or icons, but I did set up a reference configuration to send the links on the managers to the User Profile page. All that was left to do at that point was to Save the options, jump out to the Service Portal, and give this baby a try.

First test of the new widget modifications

Now that’s disturbing. There is no data in any of the aggregate columns. I hate it when that happens! Now what? Well, I always try to keep this in mind whenever these things suddenly crop up.

After quite a few trials and tribulations, I finally tracked down the source of the problem. The ng-repeat for the aggregate columns is actually nested underneath another ng-repeat for the rows in the table. Apparently, ng-repeat has some issues with arrays of primitives or strings, and prefers to deal with objects. Why that did not present itself when I was working with an array length of 1 earlier, and only now caused a problem with an array length of 3 is still a mystery to me. Fortunately, I never need to know or understand the why; I just need to know what to do about it. The solution was to convert my integer value to an object containing an integer value. So, in the core SNH Data Table widget, I changed this:

return value;

… to this:

return {value: value};

Of course, that meant that I also had to change the HTML accordingly, so I changed this:

<td ng-repeat="aggValue in item.aggValue" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
  {{aggValue}}
</td>

… to this:

<td ng-repeat="obj in item.aggValue" class="text-right" ng-class="{selected: item.selected}" tabindex="0">
  {{obj.value}}
</td>

Now let’s have another look at that new test page.

Second test of the new widget modifications

That’s better! OK, so that takes care of wrapper widget #2. Now we just need to handle the third wrapper widget, SNH Data Table from URL Definition. That sounds like a good project for our next installment.

Collaboration Store, Part XXXIV

“The key is not to prioritize what’s on your schedule, but to schedule your priorities.”
Stephen Covey

So far, we have completed the first two of the three primary components of the project, the initial set-up process and the application publication process. The last of the three major pieces will be the process that will allow you to install an application obtained from the store. Before we dive straight into that, though, we should pause to take a quick look at what we have, and what still needs to be done in order to make this a viable product. At this point, you can install all of the prerequisites and then grab the latest Update Set, install it, and go through the set-up process to create either a Host or Client instance. Once you get through all of that, you are ready to publish any local Scoped Application to the store, which will then be shared with all other instances in your Collaboration Store community.

What you cannot do, just yet, is to find an application published to the store by some other instance and install it on your own instance. That’s the missing third leg of the stool that we will need to take on next. But that is not all that is left to be done. Once we get the basics to work, there are quite a number of other things to address before one could consider this to be truly usable. Some things are just annoyances, but others are definite features that you would have to consider essential for a complete product.

Speaking of annoyances, one of the things that I really don’t like is that when you publish an app to XML for distribution, the resulting Update Set XML does not include the app’s logo image. Clearly it is a part of the app, and if you push an app to an internal store and pull it down into another instance, it comes complete with the logo, so why they left that out of the XML is a mystery to me. I don’t like that for a couple of reasons: 1) when you pull down the XML for this app, you do not get the logo, and 2) when we use the XML to publish an app to the store, the logo is missing there as well. I have seen people complain about this, but I have not, as yet, seen a solution. I would really like to address that, both for my own apps as well as for the process that we are using in this one.

Speaking of logos, another feature that I would like to have is to provide the ability for each instance to have its own distinctive logo image, so that everything from that particular instance could be tagged with that image as a way to visually identify where the app originated. That’s not a critical feature, which is why I did not include it initially, but it has always been something that I felt should be a part of the process, particularly when you start thinking about ways to browse the store and find what you are looking for. That’s definitely on the We-will-get-around-to-it-one-day list.

Browsing the store is another thing that will need some attention at some point. Right now, we just want to prove that we can set-up the app, publish an application, and install an application published by someone else. Those are the fundamental elements of the app. But once we get all of that worked out, being able to hunt through the store to find what you want will be another important capability to build out. We’re not done with the fundamentals just yet, so we don’t want to put too much energy into that issue right at the moment, but at some point, we will need to create a user-friendly way to easily find what you need.

That, of course, leads into things like searchable keywords, tags, user ratings, reviews, and the like, but now is not the time to head down that rabbit hole. Still, there are a lot of possibilities here, and this could turn into a life-long project in and of itself. That’s probably not a good thing, though!

Anyway, we won’t get anything done if we don’t focus, so we need to stay on task and figure out the application installation process. Once again, there are several options available, but the nicest one seems to be the process that you go through to install an app from an internal store. That’s basically a one-click operation and then the app is installed. Unfortunately, that particular page is neither Jelly nor AngularJS, so you can’t just peek under the hood and see the magic like you can with so many other things on the Now Platform. Another option would be to hack up a copy of the Import XML Action on the Update Set list page to push in the attached XML from a published app version, but that only takes things so far; you still have to Preview the Update Set, resolve any issues, and then manually issue the Commit. It would be much nicer if we could just push a button and have the app installation process run in the background and notify you when it was completed. Obviously, we have some work to do here to come up with the best way to go about this, and we had better figure that out relatively soon. Next time, if we are not dealing with test results from the last release, we will need to start building this out.

Collaboration Store, Part III

“It is not enough to do your best: you must know what to do, and then do your best.”
W. Edwards Deming

Today we will build the widget for the initial set-up process for the Collaboration Store app. I always like to start with the visual portion and lay things out on the screen the way that I want to see them, but before we get into that, I should explain a little bit about my basic concept for the set-up process. There are actually three independent screens involved: 1) the initial screen where you enter all of the data about your installation, 2) an email verification screen where you enter a code that was sent to your email address to verify your access to the email account, and 3) a final completion screen that lets you know that you are all set up. The HTML for the widget will include all three screens, and I will use ng-show attributes to control which section is visible at any given stage of the process. Within the widget, I will refer to that as the phase, and set up a variable called c.data.phase to track the progress through the screens.

Here is what the initial data entry screen looks like:

Initial set-up data entry screen

… and here is the HTML for that initial (phase 1) screen:

<div class="row" ng-show="c.data.phase == 1">
  <div style="text-align: center;">
    <h3>${Collaboration Store Set-up}</h3>
  </div>
  <div>
    <p>
      Welcome to the Collaboration Store set-up process.
      There are two ways that you can set up the Collaboration Store on your instance:
      1) you can be the Host Instance to which all other instances connect, or
      2) you can connect to an existing Collaboration Store with their permission.
      To become the Host Instance of your own Collaboration Store, select <em>This instance will be
      the Host of the store</em> from the Installation Type choices below.
      If you are not the Host Instance, then you will need to provide the Instance ID of the
      Collaboration Store to which you would like to connect.
    </p>
  </div>
  <form id="form1" name="form1" novalidate>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.instance_type"
          snh-name="instance_type"
          snh-label="Installation Type"
          snh-type="select"
          snh-required="true"
          snh-choices='[{"value":"host", "label":"This instance will be the Host of the store"},
                        {"value":"client", "label":"This instance will connect to an existing store"}]'/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.host_instance_id"
          snh-name="host_instance_id"
          snh-label="Host Instance ID"
          snh-required="c.data.instance_type == 'client'"
          ng-show="c.data.instance_type == 'client'"/>
        <snh-form-field
          snh-model="c.data.store_name"
          snh-name="store_name"
          snh-label="Store Name"
          snh-required="c.data.instance_type == 'host'"
          ng-show="c.data.instance_type == 'host'"/>
      </div>
    </div>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.instance_name"
          snh-name="instance_name"
          snh-label="Instance Display Name"
          snh-required="true"/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.email"
          snh-name="email"
          snh-label="Email"
          snh-type="email"
          snh-required="true"/>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.description"
          snh-name="description"
          snh-label="Instance Description"
          snh-type="textarea"
          snh-required="true"/>
      </div>
    </div>
  </form>
  <div class="row">
    <div class="col-sm-12" style="text-align: center;">
      <button class="btn btn-primary" ng-disabled="!(form1.$valid)" ng-show="c.data.instance_type == 'host'" ng-click="save();">${Create New Collaboration Store}</button>
      <button class="btn btn-primary" ng-disabled="!(form1.$valid)" ng-show="c.data.instance_type == 'client'" ng-click="save();">${Complete Set-up and Request Access}</button>
    </div>
  </div>
</div>

Basically, this is just a standard HTML form full of snh-form-fields organized into rows and columns. There are a couple of fields and a couple of buttons that are controlled by the value of that first SELECT, but other than that, it is pretty standard stuff, and there is no reason to get into any of that here.

The screen for the 2nd phase is much simpler, with only a single data entry field used to collect the code that was sent out in a Notification (more on that later) to verify the email address.

Email verification data entry screen

… and here is the HTML for the phase 2 screen:

<div class="row" ng-show="c.data.phase == 2">
  <div style="text-align: center;">
    <h3>${Email Verification}</h3>
  </div>
  <div>
    <p>
      A verification email has been sent to {{c.data.email}} with a one-time security code.
      Please enter the code below to continue.
    </p>
    <p>
      Cancelling this process will terminate the set-up process.
    </p>
  </div>
  <form id="form2" name="form2" novalidate>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.security_code"
          snh-name="security_code"
          snh-label="Security Code"
          snh-required="true"
          placeholder="Enter the security code sent to you via email"/>
      </div>
    </div>
  </form>
  <div class="row">
    <div class="col-sm-12" style="text-align: center;">
      <button class="btn btn-default" ng-disabled="!(form2.$valid)" ng-click="cancel();">${Cancel}</button>
      <button class="btn btn-primary" ng-disabled="!(form2.$valid)" ng-click="verify();">${Submit Verification Code}</button>
    </div>
  </div>
</div>

The screen for the 3rd phase is even simpler, with no data entry fields at all. It is just a message indicating that set-up is complete and was successful.

Set-up completion screen

… and here is the HTML for the phase 3 screen:

<div class="row" ng-show="c.data.phase == 3">
  <div style="text-align: center;">
    <h3>${Set Up Complete!}</h3>
  </div>
  <div>
    <p>${Congratulations!}</p>
    <p>
      The Collaboration Store set-up is now complete. Your instance has been successfully registered with the
      <b class="text-primary">{{c.data.registeredHostName}}</b> ({{c.data.registeredHost}})
      Host and is now ready to utilize the Collaboration Store features.
    </p>
  </div>
</div>

That pretty much takes care of the visual portion of the widget, which is usually the easiest part. Now we have to put all of the code underneath to make everything do all of the things that we want to do during the set-up process. One of the first things that we will want to do is to make sure that the set-up process has not already been completed. We should be able to tell that right off the bat by looking to see if there is a record in the table for the instance. If there is, then things have already been set up, and so we can artificially advance things to phase 3 and put up that final screen in the event that someone tries to go through the process a second time.

Assuming that this is the first time through, though, we will want to validate the data, and assuming that all goes well, we will want to send out the notification with the random code and advance the phase so that we can verify the email address. Once that’s done, we will need to update the database, and in the case of a client instance, we will need to register the instance with the host. To make all of that work, we will need some REST services, and probably a Script Include to contain some code to handle both sides of those inter-instance communications. We are definitely not going to get through all of that in a single installment, but we can take them all on, one issue at a time, as we work our way through all of the parts and pieces that will need to be built. Probably the easiest thing to tackle next would be the client-side code of the widget, so let’s start with that next time out.

Service Portal Form Fields, Corrected

“If you take care of the small things, the big things take care of themselves. You can gain more control over your life by paying closer attention to the little things.”
Emily Dickinson

So, I have had this annoying little bug in my Service Portal Form Fields feature that just wasn’t quite annoying enough to compel me to fix it. Lately, however, I have been doing some testing with a form that included an email field, and I finally got irritated enough to hunt down the problem and put an end to it.

The issue came about when I added the reference type fields. Under the hood, a reference type field is just a form field wrapper around an snRecordPicker. Prior to adding the reference type to the list of supported types, I displayed validation messages only if the field had been $touched or the form had been $submitted. For a reference field, though, the field is never $touched (it’s not even visible on the screen), so I added $dirty as well. That solved my problem for reference fields, but it had the unfortunate side effect of displaying the validation messages while you were still filling out the field on all other types. For fields that are simply required, that’s not a problem (as soon as you start typing, you satisfy the criteria, so there is no validation error). On fields such as email addresses, though, the first character that you type is not a valid email address, so up pops the error message before you even finish entering the data. That’s just annoying!

Anyway, the solution is obviously to only include $dirty on reference fields and leave the others as they were. Unfortunately, that is a generic line that applies all form fields of any type, so I had to includes some conditional logic in there. Here is the offending line of code:

htmlText += "      <div id=\"message." + name + "\" ng-show=\"(" + fullName + ".$touched || " + fullName + ".snhTouched || " + fullName + ".$dirty || " + form + ".$submitted) && " + fullName + ".$invalid\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";

Now, that is a huge, long line of code, so I really did not want to make it any longer by throwing some conditional logic in for the $dirty attribute. I ended up shortening it to this:

htmlText += "      <div id=\"message." + name + "\" ng-show=\"" + errorShow + "\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";

… and then building the value of the new errorShow variable ahead of that with this code:

var errorShow = "(" + fullName + ".$touched || " + fullName + ".snhTouched || ";
if (type == 'reference') {
	errorShow += fullName + ".$dirty || ";
}
errorShow += form + ".$submitted) && " + fullName + ".$invalid";

That was it. Problem solved. It turns out that it was a pretty simple fix, and something that should have been done a long, long time ago. Well, at least it’s done now. Here’s an Update Set for those of you who are into that kind of thing.

Content Selector Configuration Editor, Part V

“It takes considerable knowledge just to realize the extent of your own ignorance.”
Thomas Sowell

At the end of our last installment in this series, we had the HTML for the Tables section, but none of the underlying code to make it all work. Now it’s time to create that code by building all of the client-side functions needed to support all of the ng-click attributes sprinkled throughout the HTML. The first one that you come to in this section is the editButton function that passes the button object as an argument. This should pop open a modal editor just like we did with the Perspectives and the States, so this function could be loosely modeled after those other two. Something like this should do the trick:

$scope.editButton = function(button, btnArray) {
	var shared = {};
	if (button != 'new') {
		shared.label = button.label;
		shared.name = button.name;
		shared.heading = button.heading;
		shared.icon = button.icon;
		shared.color = button.color;
		shared.hint = button.hint;
	}
	spModal.open({
		title: 'Button/Icon Editor',
		widget: 'button-icon-editor',
		shared: shared
	}).then(function() {
		if (button == 'new') {
			button = {};
			btnArray.push(button);
		}
		button.label = shared.label;
		button.name = shared.name;
		button.heading = shared.heading;
		button.icon = shared.icon;
		button.color = shared.color;
		button.hint = shared.hint;
	});
};

We had to add a second argument to the function in this case, but only when creating a new Button/Icon. For an existing object, it is already in place in the object tree, but in the case of a new Button/Icon that we create from a new blank object, we need to know where to put it so that it lives with all of its siblings. Other than that one little wrinkle, the rest is pretty much the same as we have built before. The deleteButton function is also a little different in that we do not have a specified index, so we have to spin through the list to determine the index. Once we have that, though, all we need to do is remove the Button/Icon from the list, as there are no other dependent objects that have to be removed, unlike the Perspectives and the States.

$scope.deleteButton = function(button, btnArray) {
	var confirmMsg = '<b>Delete Button/Icon</b>';
	confirmMsg += '<br/>Are you sure you want to delete the ' + button.label + ' Button/Icon?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			var a = -1;
			for (var b=0; b<btnArray.length; b++) {
				if (btnArray[b].name == button.name) {
					a = b;
				}
			}
			btnArray.splice(a, 1);
		}
	});
};

That takes care of the Buttons/Icons … now we have to do essentially the same thing for the Reference Pages. Once again, we have a few little wrinkles that make things not quite exactly the same. For one thing, the Button/Icon data is stored in an Array, but the Reference Page data is stored in a Map. Also, both of the properties for a Reference Page (Table and Portal Page) have values that come from a ServiceNow database table, so the pop-up editor can use an sn-record-picker for both fields. That means their values will be stored in objects, not strings, so again, not an exact copy of the other functions. Still, it looks pretty close to all of its cousins:

$scope.editRefMap = function(table, refMap) {
	var shared = {table: {}, page: {}};
	if (table != 'new') {
		shared.table.value = table;
		shared.table.displayValue = table;
		shared.page.value = refMap[table];
		shared.page.displayValue = refMap[table];
	}
	spModal.open({
		title: 'Reference Page Editor',
		widget: 'reference-page-editor',
		shared: shared
	}).then(function() {
		if (table != 'new') {
			if (shared.table.value != table) {
				delete refMap[table];
			}
		}
		refMap[shared.table.value] = shared.page.value;
	});
};

… and because of those differences, the deleteRefMap function turns out to be the most simplest of all:

$scope.deleteRefMap = function(table, refMap) {
		var confirmMsg = '<b>Delete Reference Page</b>';
		confirmMsg += '<br/>Are you sure you want to delete the ' + table + ' table reference page mapping?';
		spModal.confirm(confirmMsg).then(function(confirmed) {
			if (confirmed) {
				delete refMap[table];
			}
		});
	};

So now we are done with all of the client-side functions, but our work is still not finished. Both of the edit functions pop open modal editor widgets, so we are going to need to build those. We already have a couple of different models created for the Perspective and State editors, so it won’t be as if we have to start out with a blank canvas. Still, there is a certain amount of work involved, so that sounds like a good place to start out in our next installment.

More Fun with Form Fields

“Be not simply good, be good for something.”
Henry David Thoreau

One of the things that I love about incrementally building parts is that you can obtain value from the version that you have right now, yet still leave open the possibility of adding even more value in the future. When I first set out to attempt to construct a universal form field tag, I had no idea of the ways in which that would grow, but I was able to make use of it as it was at every step along the way. The other day I had a need for field that was only required if another field contained a certain value. When I went to set that up using my latest iteration of the form field tag, I realized that the current version of the code does not support that. That’s not really a problem, though; that’s just another opportunity to create a better version!

We already have an snh-required attribute, but in the current version, it simply adds an HTML required attribute to the input element. It would seem simple enough to just replace that with an ng-required attribute instead, and we would be good to go. However, we also have the required indicator to think about — that grey/red asterisk in front of the field label. That needs to go away when something changes and the field is no longer required. But let’s keep things simple and just focus on one thing at a time. In the current version, we use an internal boolean variable called required to control what gets included in the template that we are building. We can continue to use that, keeping it boolean for false, and then making it a string for anything else. The code to do that looks like this:

var required = false;
if (attrs.snhRequired && attrs.snhRequired.toLowerCase() != 'false') {
	if (attrs.snhRequired.toLowerCase() == 'true') {
		required = 'true';
	} else {
		required = attrs.snhRequired;
	}
}

You may wonder at this point why we make it a boolean for false and a string for true, but hopefully that will reveal itself when we look at the rest of the code. The next thing that we can look at is a little snippet of code that repeats itself a few times throughout the script as it is used when building the input element for a number of different field types:

... + (required?' ng-required="' + required + '"':'') + ...

This is a conditional based on the required variable, which will resolve to true for any non-empty string, but false for the boolean value of false. If the value of required is not false, then we use that same variable, which we now know is a string, to complete the ng-required attribute value for the input element. This will work for values of ‘true’ just as easily as for values that contain some kind of complex conditional statement. This was the easy part, and all of the logic was resolved within the code that generates the template.

The required indicator is another story entirely. Since some condition can toggle the element from required to not required, that same condition needs to apply to the required indicator as well. If the value of the snh-required attribute is anything other than simply ‘true’ or false, we will have to incorporate that logic in the indicator element to determine whether or not to show the indicator image. That code now looks like this:

htmlText += "          <span id=\"status." + name + "\"";
if (required) {
	if (required == 'true') {
		htmlText += " ng-class=\"" + model + refExtra + ">''?'snh-required-filled':'snh-required'\"";
	} else {
		htmlText += " ng-class=\"(" + required + ")?(" + model + refExtra + ">''?'snh-required-filled':'snh-required'):''\"";
	}
}
htmlText += "></span>\n";

As before, the first conditional is based on the required variable, and if it is false, then we don’t do anything at all. But in this case, we also have to check to see if it is equal to the string ‘true’, because if it is, we can just do what we were doing before to make this work. If it is not, then we have to include the required condition in the rendered ng-class attribute to toggle the indicator off and on at the same time that the field requirement is being toggled off and on. When the field is not required, the indicator should just go away, and when it is required, it should be there, and it should be red if the field is empty and grey if it has a value.

That’s it for the code changes. To test it, I brought up my old friend the, form field testing widget, and then altered my textarea example to look like this:

<snh-form-field
  snh-model="c.data.textarea"
  snh-name="textarea"
  snh-label="Text Area"
  snh-type="textarea"
  snh-required="c.data.select==2"
  maxlength="20"
  snh-help="This is where the field-level help is displayed. This field should be required if Option #2 is selected above."/>

That should make the textarea field required when Option #2 is selected on the field that is now above:

The textarea is required when Option #2 is selected

… and it should not be required when anything else is chosen:

The textarea is not required when Option #2 is not selected

With everything looking pretty solid, I was ready to generate another Update Set and call it good. Instead, I thought maybe I would just try a few more things, just to be sure. That’s when I discovered the problem.

It’s always something. I tinkered with the last name field to make it only required if the first name value was greater than spaces, and suddenly all of the form fields that followed within that same DIV disappeared.

Missing form fields

That’s not right. Not only is it not right, I have no idea why that is happening. If you put a greater than (‘>’) sign in the value of the snh-required attribute, anything that follows in the same DIV evaporates. I tried a number of things to fix it, and I found quite a few ways to work around it, but I was never able to actually solve the problem. I hate releasing something that has this kind of a bug in it, but since I don’t seem to posses the mental capacity required to remove the flaw at this particular moment in time, that’s what I am going to end up doing. There are work-arounds, though, so I don’t feel that bad about it. Here are some of the ones that worked for me:

  • Enclose the snh-form-field tag with a DIV. Since the problem only wipes out things within the same DIV, if it is the only thing in the DIV, the problem goes away. I actually tried to do that within the template itself, but that doesn’t work; the DIV has to be outside of the tag, not part of the code that is generated by the tag.
  • Encode the greater than sign. Actually, you have to double encode it, as &gt; will not work, but &amp;gt; does the trick. Not my idea of an intuitive solution, but it does work. And again, I tried to do that within the template itself, but that does nothing at all.
  • Don’t use a greater than sign. In my own example, I could have used snh-required=”c.data.firstName” and that would have worked just as well as snh-required=”c.data.firstName>””. Also, you can call a function that contains the greater than condition, which keeps it out of the attribute value as well.

Again, these are just work-arounds. In my mind, you shouldn’t have to do that. Hopefully, in some future version, you won’t. But if you want to play around with it the way that it is, here is the latest Update Set.

Update: There is a better (enhanced) version here (… but it still doesn’t address the > issue.)

Formatted Script Search Results, Corrected

“Never give in, never give in, never, never, never, never.”
Winston Churchill

For the most part, I really liked the way my formatted search result widget came out; however, the fact that I lost my results after following the links on the page was something that I just couldn’t accept. In my rush to get that out, I released a version with that obvious flaw, but I couldn’t just let that be the end of it; there had to be a way to make it remember its way back home without losing track of what was there before. There just had to be.

I tried a lot of things. They say that when you lose something, you always find it in the last place that you look. I always wondered why that was considered such profound wisdom, since it seemed quite obvious to me that once you found it, you would stop looking. Of course you always find it in the last place that you looked. When I finally came across a way to get the results that I wanted, I stopped looking. There may be a better way to achieve the same ends, but once I was able to link out to a page and come back to my original results, I was done. I’m not all that proud of the code, but it works, so I’m good.

The problem turned out to be this statement, which takes you to the next URL, which includes the search string parameter:

$location.search('search', c.data.searchFor);

This uses the AngularJS $location service, which is quite powerful, but apparently not quite powerful enough to leave a trail that you could follow back home again. I tried a number of variations to get things to work, but in the end I abandoned this whole approach and just went full old school. I replaced my ng-click with an onclick, and in my onclick function, I replaced that nice, simple one line of code with several others:

function findIt() {
	var url = window.location.pathname + window.location.search;
	if (url.indexOf('&search=') != -1) {
		url = url.substring(0, url.indexOf('&search='));
	}
	url += '&search=' + document.getElementById('searchFor').value;
	window.location.href = url;
}

Now I will be the first one to admit that this is quite a bit more code than that one simple line that I had before. First, I had to glue together the path and the search string to construct a relative URL, then I had to check to see if a search parameter was already present, and if so, clip it off of the end, then add my new search parameter to the search string, and then finally, set the current location to the newly constructed URL. Aesthetically, I prefer the original much, much better, but this older, brute force method has the advantage of actually working the way that I want, so it gets to be the winner.

I still kept my ng-click, but that was just to toggle on a new loading DIV to let the user know that their input was accepted and now we are working on getting them their results. That simple HTML addition turned out like this:

<div class="row" ng-show="c.data.loading">
  <div class="col-sm-12">
    <h4>
      <i class="fa fa-spinner fa-spin"></i>
      ${Wait for it ...}
    </h4>
  </div>
</div>

One other thing that I tinkered with in this iteration was the encoded query string in the Script Include. There is a particular table (sn_templated_snip_note_template) that kept throwing an error message related to security, so I decided to just filter that one out by name to keep that from happening. The new encoded query string now looks like this:

internal_typeCONTAINSscript^active=true^name!=sn_templated_snip_note_template

There might be a few other clean-up odds and ends included that I can’t quite recall right at the moment, but the major change was to finally get it to come back home again after drilling down into one the listed scripts. If you installed the previous version of this Update Set, I would definitely recommend that you replace it with this one.