Collaboration Store, Part LXXV

“Imagination is the beginning of creation. You imagine what you desire, you will what you imagine and at last you create what you will.”
George Bernard Shaw

While we wait patiently for some comments from the folks gracious enough to do some testing with the latest release, let’s take a quick peek ahead something that we might want to tackle next. We are rapidly winding down the initial goal of getting all of the basic mechanics working for installing, setting up, and using the app to move artifacts between instances. Once we have all of that working satisfactorily, we will want to expand our efforts to include some of those other items on our list of things that we would still like to do. One of the major items on that list is the shopping experience, or the way in which a developer would locate an app in the store. Just focusing on the presentation itself for just a moment, let’s take a look at some of the many similar experiences for that there are right now.

All Applications

If you select plugins on the primary navigation, you get redirected to the All Applications page.

All Applications Page

This page features a left-hand panel for various filter options and then a right-hand panel of full-width application tiles that include details about the app and two installation options, scheduled and immediate. In the upper right-hand corner there is a Find in Store button, which will take you to our next example.

App Store

ServiceNow App Store

This one also has a left-hand filter column and a right-hand column of application tiles, but on this one the tiles are not full-width and there are no install options. There is a cool mouse-over feature, though, and this one includes user-ratings, which can also be used as filter and sort options.

My Company Applications

Now Platform Application Manager

The application manager built into the Now Platform uses tabs rather than filters, and the application tiles are full width, but do not include the description of the app. Rather than install options there is an Edit button, which makes it look similar in appearance to the first example.

Service Catalog

Not an application shopping experience, but a shopping experience just the same, and with many characteristics similar to those of the other examples.

ServiceNow Service Catalog

This one has a left-hand panel for selecting a category from a category tree and the tiles in the right-hand panel are more like those in the App Store. Obviously, the content of the tiles would have to be adjusted to include the relevant data for applications, but the one benefit of this one over the others is that we have access to the source code. This is a standard Service Portal Page, which we could potentially clone and then modify for our purpose. The same is also true of the individual widgets that make up the page, as they could also be either cloned or replaced.

At this stage of the process, we do not have many of the features that make up much of the content in some of these examples. Today, there are no categories, no tags, no reviews, no ratings, and no other helpful ways to filter the list down to the things in which you might have an interest. Introducing any such features would be a project in and of itself, but we can still attempt to build a rudimentary shopping experience without those, and then throw those in later as the needs arise. For now, there aren’t that many test apps in the working model, so filtering is not yet a pressing need.

Just to see what we can do, let’s grab a copy of the catalog portal page next time and start hacking it up to meet our needs. It won’t necessarily be everything that it could be right at the start, but it will be a beginning.

Scripted Value Columns, Enhanced, Part III

“It’s not about ideas. It’s about making ideas happen.”
Scott Belsky

Last time, we played around with the Customer column in our new example data table, and today we will jump back over to the Labor Hours column and see if we can create that mouse-over breakdown of who put in the hours. The first thing that we will need to do is to gather up the data, so will need to add a groupBy to our GlideAggregate so that we can obtain the hours per technician.

var timeGA = new GlideAggregate('time_card');
timeGA.addQuery('task', item.sys_id);
timeGA.addAggregate('SUM', 'total');
timeGA.groupBy('user');
timeGA.orderBy('user');
timeGA.query();

Once we have the data, we will need to format it for display. There are a number of different ways that we can approach this, including straight CSS and many creative variations, but the easiest thing to do is to just use the title attribute of a span, which seems to accomplish the same thing.

Mouseover breakdown of labor hours

To create the text, we can establish some variables before we go into the while loop, and then inside the loop we can build up the text, one person’s hours at a time.

var hours = 0;
var tooltip = '';
var separator = '';
var timeGA = new GlideAggregate('time_card');
timeGA.addQuery('task', item.sys_id);
timeGA.addAggregate('SUM', 'total');
timeGA.groupBy('user');
timeGA.orderBy('user');
timeGA.query();
while (timeGA.next()) {
	var total = timeGA.getAggregate('SUM', 'total') * 1;
	hours += total;
	tooltip += separator + timeGA.getDisplayValue('user') + ' - ' + total.toFixed(2);
	separator = '\n';
}

Once we accumulate the total hours and build up the tooltip text, we can then format the HTML we want, but only when there are hours charged to the task.

if (hours > 0) {
	response = '<span style="text-align: right; width: 100%;" title="' + tooltip + '">' + hours.toFixed(2) + '</span>';
}

Putting it all together, our new function to provide right-justified total hours with a mouseover breakdown looks like this:

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

	var hours = 0;
	var tooltip = '';
	var separator = '';
	var timeGA = new GlideAggregate('time_card');
	timeGA.addQuery('task', item.sys_id);
	timeGA.addAggregate('SUM', 'total');
	timeGA.groupBy('user');
	timeGA.orderBy('user');
	timeGA.query();
	while (timeGA.next()) {
		var total = timeGA.getAggregate('SUM', 'total') * 1;
		hours += total;
		tooltip += separator + timeGA.getDisplayValue('user') + ' - ' + total.toFixed(2);
		separator = '\n';
	}
	if (hours > 0) {
		response = '<span style="text-align: right; width: 100%;" title="' + tooltip + '">' + hours.toFixed(2) + '</span>';
	}

	return response;
}

I think that is enough examples for folks to get the basic idea. Adding the ability to include HTML with a scripted value opens up a number of possibilities. We have just explored a couple of them here, but I am sure that specific requirements will drive many other variations from those willing to give it a try and see what they can do with it.

Here is an Update Set with the modifications, including this new example page. Feedback can be left here in the comments, or in the discussion area where it has been posted out on Share. If you have been able to utilize this feature for anything interesting, a screenshot would definitely be something in which folks would have an interest, so please let us all in on what you were able to accomplish.

Scripted Value Columns, Part VIII

“Too many men work on parts of things. Doing a job to completion satisfies me.”
Richard Proenneke

Last time, we wanted to wrap up the modifications on the last two wrapper widgets and put out a new Update Set, but we discovered that we missed an important element in our list of things that would need to be modified, the Configurable Data Table Widget Content Selector widget. We need to take a look at that guy and see what needs to be done to accommodate scripted value columns, and then retest the third wrapper widget, which shares the page with this component.

A quick scan of the Server script for aggarray comes up empty, but in the Client script, we come across this:

s.aggregates = '';
if (tableInfo[state].aggarray && Array.isArray(tableInfo[state].aggarray) && tableInfo[state].aggarray.length > 0) {
	s.aggregates = JSON.stringify(tableInfo[state].aggarray);
}

Making a copy of that and doing a little string replacement here and there gives us an equivalent block of code for the new scripted value column configurations.

s.scripteds = '';
if (tableInfo[state].svcarray && Array.isArray(tableInfo[state].svcarray) && tableInfo[state].svcarray.length > 0) {
	s.scripteds = JSON.stringify(tableInfo[state].svcarray);
}

And that seems to be all there is to that. Now we can go back to our last test and run it again to see if that fixed our problem.

Successful test of the third wrapper widget

That’s better! Now we have a column for the Last Comment, and we even have a row with some data in it. Good deal. And just to check on the content selector widget, we can look at the URL that it built to see how the configuration options for the scripted value columns appeared in the URL.

/sp?id=my_things&table=incident&filter=caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe^active%3Dtrue&fields=number,opened_by,opened_at,short_description&scripteds=[{"heading":"Last Comment","name":"last_comment","label":"Last Comment","script":"global.ScriptedJournalValueProvider"}]&aggregates=&buttons=&refpage={"sys_user":"user_profile"}&px=requester&sx=open&spa=1&p=1&o=opened_at&d=asc

Well, that’s the whole thing, but we can zoom in on the part in which we have an interest.

scripteds=[{"heading":"Last Comment","name":"last_comment","label":"Last Comment","script":"global.ScriptedJournalValueProvider"}]

So that is the last of the wrapper widgets, and unless we have left something else out, that’s the last of the work to be done to implement this new feature. Now all that is left is to bundle the whole thing up into a new Update Set and post it out on Share as a new version.

Here is the new Update Set, and here is where you can find it on Share. If you happen to use it, find it to be of value, or run into any issues, please let us all know in the comments below.

Scripted Value Columns, Part V

“Make incremental progress; change comes not by the yard, but by the inch.”
Rick Pitino

Last time, we had enough parts cobbled together to demonstrate that the concept actually works. Of course, all we had to show for it was some random numbers, but that told us that the specified script was being called for each row, which is what we were after. Now that we know that the basic structure is performing as desired, we can revisit the configurable Script Include component and see if we can come up with some actual use cases that might be of value to someone.

One of the questions that triggered this idea was related to comments and work notes on Incidents. Assuming that the main record in the table is an Incident, we can clone our example Script Include to create one dedicated to pulling data out of the latest comment or work note on an Incident. We can call this new Script Include ScriptedJournalValueProvider.

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

	getScriptedValue: function(item, config) {
		return Math.floor(Math.random() * 100) + '';
	},

	type: 'ScriptedJournalValueProvider'
};

We will want to delete the example code in the getScriptedValue function and come up with our own, but other than that, the basic structure remains the same. Assuming that we want our script to be able to handle a number of attributes of an Incident Journal entry, we can use the name of the column to determine which function will fetch us our value.

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

	var column = config.name;
	if (column == 'last_comment') {
		response = this.getLastComment(item, config);
	} else if (column == 'last_comment_by') {
		response = this.getLastCommentBy(item, config);
	} else if (column == 'last_comment_on') {
		response = this.getLastCommentOn(item, config);
	} else if (column == 'last_comment_type') {
		response = this.getLastCommentType(item, config);
	}

	return response;
}

This way, we can point to this same script in multiple columns and the name of the column will determine which value from the last comment or work note gets returned.

Since all of the functions will need the data for the last entry, we should create a shared function that they all can leverage to obtain the record. As with many things on the ServiceNow platform, there are a number of ways to go about this, but for our demonstration purposes, we will read the sys_journal_field table looking for the last entry for the Incident in the current row.

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

Now that we have a common way to obtain the GlideRecord for the latest entry, we can start building our functions that extract the requested data value. Here is the one for the comment text.

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

	var journalGR = this.getLastJournalEntry(item.sys_id);
	if (journalGR.isValidRecord()) {
		response = journalGR.getDisplayValue('value');
	}

	return response;
}

The others will basically be copies of the above, modified to return different values based on their purpose. The whole thing, all put together, now looks like this.

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

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

		var column = config.name;
		if (column == 'last_comment') {
			response = this.getLastComment(item, config);
		} else if (column == 'last_comment_by') {
			response = this.getLastCommentBy(item, config);
		} else if (column == 'last_comment_on') {
			response = this.getLastCommentOn(item, config);
		} else if (column == 'last_comment_type') {
			response = this.getLastCommentType(item, config);
		}

		return response;
	},

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

		var journalGR = this.getLastJournalEntry(item.sys_id);
		if (journalGR.isValidRecord()) {
			response = journalGR.getDisplayValue('value');
		}

		return response;
	},

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

		var journalGR = this.getLastJournalEntry(item.sys_id);
		if (journalGR.isValidRecord()) {
			response = journalGR.getDisplayValue('sys_created_by');
		}

		return response;
	},

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

		var journalGR = this.getLastJournalEntry(item.sys_id);
		if (journalGR.isValidRecord()) {
			response = journalGR.getDisplayValue('sys_created_on');
		}

		return response;
	},

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

		var journalGR = this.getLastJournalEntry(item.sys_id);
		if (journalGR.isValidRecord()) {
			response = journalGR.getDisplayValue('element');
		}

		return response;
	},

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

	type: 'ScriptedJournalValueProvider'
};

Now that we have a Script Include to utilize, we need to put together a new page so that we can configure it to make use of it so that we can test it out. Let’s make a quick copy of the page that we were using for testing last time and call it scripted_value_test. Also, let’s make a quick copy of the test configuration script that we were using earlier and call it ScriptedValueConfig.

var ScriptedValueConfig = Class.create();
ScriptedValueConfig.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: 'caller_idDYNAMIC90d1921e5f510100a9ad2572f2b477fe^active=true',
				fields: 'number,opened_by,opened_at,short_description',
				svcarray: [{
					name: 'last_comment_on',
					label: 'Last Comment',
					heading: 'Last Comment',
					script: 'global.ScriptedJournalValueProvider'
				},{
					name: 'last_comment_by',
					label: 'Last Comment By',
					heading: 'Last Comment By',
					script: 'global.ScriptedJournalValueProvider'
				}],
				aggarray: [],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			}
		}]
	},

	type: 'ScriptedValueConfig'
});

Now let’s pull up our new page in the Service Portal Designer and point the table widget to our new configuration script.

Configuring the new test page to use the new test configuration script

Once we save that, we can pop over to the Service Portal and pull up our new page to try it out.

First test of our first real world utilization of this feature

Beautiful! Our new scripted value provider Script Include was called by the core SNH Data Table widget and it returned the requested values, which were then displayed on the list with all of the other standard table columns. That wasn’t so hard, now was it?

Of course, we still have a couple more wrapper widgets to modify (and test!), and I would like to produce another example, maybe something to do with catalog item variables, but I think we are close. One thing I see that I never noticed before, though, is that the added columns don’t quite line up with the original columns. Maybe it is a CSS thing, or maybe it is something a little more diabolical, but I want to take a look at that and see what is going on there. All of the data in the columns should be displayed consistently; I don’t like it when things don’t all line up correctly. I need to figure out what is going on there and see what I can do about it.

Anyway, we still have a little more work to do before we can wrap this all up into a new Update Set and post a new version out on Share, but we will keep plugging along in our next installment.

SNH Data Table Widgets on Share

“The first time you do a thing is always exciting.”
Agatha Christie

I’ve never used Share before, but after completing the work on the Aggregate List Columns and bundling that work up with all of the other related projects and artifacts, I decided that I would go ahead and post the whole thing out there. I have always hesitated to throw stuff from here out there, mainly because most of the things that you will find on this site are not very well documented, at least not from the user’s perspective. But, I have considered doing it anyway on a number of occasions. I was pretty close to sharing the My Delegates Widget until I discovered that someone else had already beat me to it. I also thought about tossing out a number of other items such as the Dynamic Service Portal Breadcrumbs and the Service Portal Widget Help, but like quite a number of other things, those were just thoughts that never turned into any kind of action. This time, though, quite a number of things were all bundled together into a single Update Set, and I thought that maybe there just might be a thing or two buried in there somewhere that someone somewhere might find to be of value. We’ll see.

My other hesitation to posting this on Share was the fact that these are all Service Portal components, and ServiceNow has made it pretty clear that they would like to see folks abandon the Service Portal in favor of their latest approach to application development. While it may be true that the Service Portal is on the way out, it has been my experience that such transitions usually take some time to be fully realized, so there still may be an active Service Portal or two floating around out there for a while. Still, everyone always likes to jump on the new stuff, so the interest in Service Portal components is something that is bound to start dropping off over time. On the other hand, that actually serves as an argument for shoving it out there now, as waiting around would just mean even less relevance to the environment of the future.

Anyway, it’s done now. Share obviously has a much broader reach than this little blog, so it will be interesting to see if anyone happens to come across it out there with all of the other artifacts on the site. I did take a quick peek this morning, and it does look like a couple of brave souls have already hit the download button, but I don’t see any feedback posted as yet. That will probably take a little more time. Who knows; if it all works out, maybe one day I will throw something else out there. Only time will tell.