Hacking the Scripted REST API, Part II

“If you find a path with no obstacles, it probably doesn’t lead anywhere.”
Frank A. Clark

Last time, we built a Scripted REST API with a single Scripted REST Resource. To finish things up, we just need to add the actual script to the resource. To process our legacy support form, create an Incident, and respond to the user, the script will need to handle the following operations:

  • Obtain the input form field values from the request,
  • Format the data for use in creating the Incident,
  • Locate the user based on the email address, if one exists,
  • Create the Incident, and
  • Return the response page.

Obtain the input form field values from the request

This turned out to be much simpler than I first made it out to be. I knew that the out-of-the-box Scripted REST API was set up to handle JSON, both coming in and going out, and could also support XML, but I never saw anything regarding URL encoded form fields, so I assumed I would have to parse and unencode the request body myself. The problem that I was having, though, was getting the actual request body. I tried request.body, request.body.data, request.body.dataString, and even request.body.dataStream — nothing produced anything but null or errors. Then I read somewhere that that the Scripted REST API treats form fields as if they were URL query string parameters, and lumps them all together in a single map: request.queryParams. Once I learned that little tidbit of useful information, the rest was easy.

// get form data from POST
var formData = request.queryParams;
var name = formData.name[0];
var email = formData.email[0];
var short_description = formData.title[0];
var description = formData.description[0];

It did take me a bit to figure out that the values returned from the map are arrays and not strings, but once that became clear in my testing, I just added an index to the form field name and everything worked beautifully.

Format the data for use in creating the Incident

This was just a matter of formatting a few of the incoming data values with labels so that anything that did not have a field of its own on the Incident could be included in the Incident description.

// format the data
var full_description = 'The following issue was reported via the Legacy Support Form:\n\n';
full_description += 'Name: ' + name + '\n';
full_description += 'Email: ' + email + '\n\n';
full_description += 'Details:\n' + description;

Locate the user based on the email address

This doesn’t really have much to do with hacking the Scripted REST API, but I threw it in just as an example of the kind of thing that you can do once you have some data with which to work. In this case, we are simply using the email address that was entered on the form to search the User table to see if we have a user with that email. If we do, then we can use as the Caller on the Incident.

// see if we have a user on file with that email address
var contact = null;
var userGR = new GlideRecord('sys_user');
if (userGR.get('email', email)) {
	contact = userGR.getUniqueValue();
}

Create the Incident

This part is pretty vanilla as well.

// create incident
var incidentGR = new GlideRecord('incident');
if (contact) {
	incidentGR.caller_id = contact;
} else {
	incidentGR.caller_id.setDisplayValue('Guest');
}
incidentGR.contact_type = 'self-service';
incidentGR.short_description = short_description;
incidentGR.description = full_description;
incidentGR.assignment_group.setDisplayValue('Service Desk');
incidentGR.insert();
var incidentId = incidentGR.getDisplayValue('number');

The last line grabs the Incident number from the inserted Incident so that we can send that back to the user on the response page.

Return the response page

Now that we have complete all of the work, the last thing left to do is to respond to the user. Again, since we are not using the native JSON or XML formats, we are going to have to do some of the work a little differently than the standard Scripted REST API. Here is the working code:

// send response page
response.setContentType('text/html');
response.setStatus(200);
var writer = response.getStreamWriter();
writer.writeString(getResponsePageHTML(incidentId));

The first thing that you have to know is that you must set the content type and status before you get the stream writer from the response. If you don’t do that first, then things will not work. And even though you clicked the override checkbox and specified text/html as the format in the Scripted REST API definition, you still have to set it here as well. But once you do all of that, and do it in the right sequence, it all works great.

The response string itself is just the text of a standard HTML page. I encapsulated my sample page into a function so that it could be easily replaced with something else without disturbing any of the rest of the code. The sample version that I put together looks like this:

function getResponsePageHTML(incidentId) {
	var html = '';

	html += '<html>';
	html += ' <head>';
	html += '  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">';
	html += '  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>';
	html += ' </head>';
	html += ' <body>';
	html += '  <div style="padding: 25px;">';
	html += '   <h2>Thank you for your input</h2>';
	html += '   <p>We appreciate your business and value your feedback. Your satisfaction is of the utmost concern to us.</p>';
	html += '   <p>Your issue has been documented and one of our marginally competent technicians will get back to you within a few months to explain to you what you have been doing wrong.</p>';
	html += '   <p>Your incident number is ' + incidentId + '.</p>';
	html += '  </div>';
	html += ' </body>';
	html += '</html>';

	return html;
}

That’s it for the coding in ServiceNow. To see how it all works, we just need to point our old form to our new form processor after which we can pull up the modified form in a browser and give it a try. To repoint the legacy form, pull it up in a text editor, find the form tag, and enter the path to our Scripted REST API in the action attribute:

<form action="https://<instance>.service-now.com/api/<scope>/legacy/support" method="post">

With that little bit of business handled, we can pull up the form, fill it out, click on the submit button, and if all goes well, be rewarded for our efforts with our HTML response, including the number of the newly created Incident:

HTML response from the Scripted REST API form processor

Just to be sure, let’s pop over to our instance and check out the Incident that we should have created.

Incident created from submitting the legacy form

Well, that’s all there is to that. We built a Scripted REST API that accepted standard form fields as input and responded with a standard HTML web page, and then pointed an old form at it to produce an Incident and a thank you page. All in all, not a bad little perversion of the REST API feature of the tool!

Hacking the Scripted REST API to process a form

“Try something new each day. After all, we’re given life to find it out. It doesn’t last forever.”
Ruth Gordon

One of our older systems includes a form through which you could report issues, and when you filled out the form and submitted it, it would send an e-mail to the support team. Pretty cool stuff back in the day, but the procedure bypasses the Service Desk and hides the activity from our support statistics because no one ever opens up an Incident. They just quietly resolve the issue and move on without a trace. The people who like to have visibility into those kinds of activities are not really too keen on these little side deals that allow certain groups to fly below the radar. So the question arose as to whether or not we could keep the form, with which everyone was comfortable and familiar, but have it create an Incident rather than send an e-mail.

Well, the first thing that came to mind was to just send the existing e-mail to the production instance and then set up an inbound mail processor to turn the e-mail into an Incident. The problem with that approach, though, was the the Incident was created off-line, and by that time, you had no way to inform the user that the Incident was successfully created or to give them the ID or some kind of handle to pull it up and check on the progress. What would really be nice would be to be able to simply POST the form to ServiceNow and have it respond back with an HTML thank you page. ServiceNow is not really set up to be a third-party site forms processor, though, so that really didn’t seem to be a feasible concept.

But, then again …

ServiceNow does have the Scripted REST API, but that is built for Web Services, not user interaction. Still, with a little creative tweaking maybe we could actually fool it into taking a form POST and responding with an HTML page. That would actually be interesting. And as it turns out, it wasn’t all that hard to do.

To make our example relatively simple and easy to follow, let’s just build a nice clean HTML page that contains nothing but our example legacy form:

Simple stand-alone input form for demonstration purposes

This clears out all of the window dressing, headers, footers, and other clutter and just gets down to the form itself. None of that other stuff has any relevance to what we are trying to do here, so we just want a simple clean slate without all of the distractions. Here is the entire HTML code for the page:

<html>
 <head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
 </head>
 <body>
  <form action="/some/legacy/form/handler" method="post">
   <div style="padding: 25px;">
    <h2>Legacy Support Form</h2>
    <div style="margin-top: 10px;">
     <label for="name">Please enter your name:</label>
    </div>
    <div>
     <input name="name" size="64"/>
    <div style="margin-top: 10px;">
     <label for="email">Please enter your email address:</label>
    </div>
    <div>
     <input name="email" size="64"/>
    </div>
    <div style="margin-top: 10px;">
     <label for="title">Please enter a brief statement describing the issue:</label>
    </div>
    <div>
     <input name="title" size="64"/>
    </div>
    <div style="margin-top: 10px;">
     <label for="description">Please describe the problem in detail:</label>
    </div>
    <div>
     <textarea name="description" cols="62" rows="5"></textarea>
    </div>
    <div style="margin-top: 20px;">
     <input type="submit" value="Submit Problem Report"/>
    </div>
   </div>
  <form>
 </body>
</html>

The idea here is to now take the existing form handler, as specified in the action attribute of the form tag, and replace it with a URL for a ServiceNow “web service” that we will create using the Scripted REST API tools. That is the only change that we want to make on this existing page. Everything else should look and behave exactly as it did before; we are just pointing the form to a new target for processing the user input. So let’s build that target.

To begin, select the Scripted REST APIs option on the left-hand sidebar menu, which will bring up the list of all of the existing Scripted REST APIs. From there, click on the New button, which will take you to a blank form on which you can start entering the details about your new Scripted REST API.

Initial Scripted REST API data entry form

Enter the name of your new Scripted REST API along with the API ID, which will become a component in the eventual URL that will take you to your new web service. Once you have saved these initial values, you will be returned to the list, and your newly created API will appear on the list. Select it, and you will be taken to an expanded form where you can now enter the rest of the information.

Full Scripted REST API data entry form

Here is one of the secrets to our little hack: you need to check both of the override boxes so that you can change the MIME type for the data that will be flowing in both directions. For the data that is coming in, you will want to enter x-www-form-urlencoded. These are are the form fields coming in from the input form. For the data going out, you will want to enter text/html. This is the response page that will go back to the browser and be rendered by the browser for display to the user. Form fields come into our new service and HTML comes back out.

Once you have saved your overrides, you can create your Resource. A Scripted REST API can have one or more Resources, and they appear on the form down at the bottom as a Related List. Open up the Resources tab and click on the New button to create your Resource. This brings up the Resource form.

Scripted REST Resource form

On this form, you want to enter the name of your Resource, the HTTP method (POST), and the relative path, which is another component of the URL for this service. Once you save that, all that is left is my favorite part, the coding. In fact, this is probably a good place to stop, as we are done with all of the forms and form fields. Next time out, we can focus exclusively on the code.

Formatted Script Search Results, Corrected (again!)

“Creativity is allowing yourself to make mistakes. Art is knowing which ones to keep.”
Scott Adams

In my haste to release my enhanced search tool that I tweaked to search both Script and HTML, I neglected to hunt down all of the places where I used the word Script and fix it up so that it would only say Script when we were searching Script and say HTML when we were searching HTML. I was aware that I had missed that on the button at the time that I published the Update Set, as you could easily see the problem right there on the image that I posted.

The title says HTML, but the button still says Search Scripts

But it turns out that I also had the same problem with the message that comes out if you don’t find anything, and with the help text above the entry field (although, that one I did try to make generic so that it would apply to both). I hate to release a new version just for a couple of bad labels, but it bugs me that it isn’t right, so I wanted to tidy that up and make it right.

To fix it, I swapped my title variable with a more generic label variable and then used that label to build the page title, the button text, and a number of different message. Now everything said Script when we were dealing with script and HTML when we were dealing with HTML. Still, that little change was hardly worth a new version, so I decided to add a couple of features that I thought were missing earlier. I added a count of records above the search results table, and I also sorted the table, since it seemed to be coming out in a quite random fashion. Still minor improvements, but now we had more of a combo Correction/Enhancement release that both fixed a couple of issues and also added a couple of new features.

Search results with consistent labels, row counter, and sorted output

While testing all of that out, I actually uncovered a couple of other minor issues as well, which I also corrected, so this one is actually much improved over the last. I won’t waste space here going into all of the details, but here is the new Update Set, and if you are really interested, you can just do a compare against the last one.

Formatted Script Search Results, Enhanced

“Delivering good software today is often better than perfect software tomorrow, so finish things and ship.”
— David Thomas, The Pragmatic Programmer: From Journeyman to Master

It always seems to happen. The more I play around with some little bauble that I’ve thrown together, the more ideas that pop into my head to make it just a little bit better. I’ve actually found my script search tool to be quite useful now that I have finally gotten it to behave the way in which I would expect it to behave. But the more that I use it, the more that I find that I want add to it.

One thing that I realized after I released the code was that you can also find script in Condition String fields. This blows my whole theory that all column types that contain script have the word script in their name. Still, I can tweak my encoded query just a bit to ensure that these columns are searched as well. I just need to switch from this:

internal_typeCONTAINSscript^active=true^name!=sn_templated_snip_note_template

… to this:

active=true^name!=sn_templated_snip_note_template^internal_typeCONTAINSscript^ORinternal_type=condition_string

Another thing that crossed my mind while I was searching for something the other day was that, in addition to scripts, I would really like to have this same capability for searching HTML. At the time, it seemed like it wouldn’t be too difficult to just clone all of the parts and convert them to a second set that was dedicated to HTML instead of Javascript. When I took a look at doing that, though, I realized that with just little bit of extra code, I could make the parts that I had work for both, and not have to create an entirely new second set.

The first thing that I tackled was my Script Include, ScriptUtils, To start out, I renamed it to SearchUtils, since it was now going to handle both searching all of the script columns and searching all of the HTML columns. Then I added a second argument to the function call called type, so that I could differentiate between script search requests and HTML search requests. The only real difference in the code between searching for script and searching for HTML is which columns on which tables will be queried, which means that the only real difference between the two operations is the query used to find the columns and tables. In the current version, that query was hard-coded, so I switched that to a variable and then set the value of the variable to the new version of my encoded query for scripts, and then overrode that value with a new query for HTML if the incoming type argument was set to ‘html’.

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

	findInScript: function(string, type) {
		var table = {};
		var found = [];
		var encodedQuery = 'active=true^name!=sn_templated_snip_note_template^internal_typeCONTAINSscript^ORinternal_type=condition_string';
		if (type == 'html') {
			encodedQuery = 'active=true^name!=sn_templated_snip_note_template^internal_typeCONTAINShtml';
		}
		var columnGR = new GlideRecord('sys_dictionary');
		columnGR.addEncodedQuery(encodedQuery);
...

Beyond that point, everything else remains the same as it was in the earlier version. That takes care of the Script Include. Now we have to tackle the associated Widget. Here we can use the same basic philosophy, defaulting everything to a script search, and then overriding that default if an HTML search is requested. For a Widget, that request can come in the form of an additional URL parameter that we can also call type. We can establish the default values based on a script search, and then if the URL parameter type is equal to ‘html’, we can then change to the values for an HTML search.

(function() {
	data.type = 'script';
	data.title = 'Script Search';
	if ($sp.getParameter('type') && $sp.getParameter('type') == 'html') {
		data.type = 'html';
		data.title = 'HTML Search';
	}
	data.loading = false;
	data.searchFor = '';
	data.result = false;
	if ($sp.getParameter('search')) {
		data.loading = true;
		data.searchFor = $sp.getParameter('search');
		data.result = new SearchUtils().findInScript(data.searchFor, data.type);
		data.loading = false;
	}
})();

That was all there was to adding an HTML version of the script searching capability. To get to it, I pulled my Search Script menu item up in edit mode by clicking on the little pencil to the right of the title, made a few minor modifications and then selected Insert from the context menu to create a new menu item.

Cloning the Search Script menu item to create the Search HTML menu item

With the new menu item added to the sidebar, the only thing left to do was to click on the item and give it a go.

New HTML Search function in action

Well, that didn’t turn out too bad. I should have changed the name of the button to something that didn’t have the word script in it, but other than that one little nit, it all seems to work. I’ll fix up that button one day, but for now, here is an Update Set that includes all of the parts for the current version.

User Rating Scorecard, Part IV

“You can’t have everything. Where would you put it?”
Steven Wright

Well, I had a few grand ideas for my User Rating Scorecard, but not all ideas are created equal. Some are quite a bit easier to turn into reality than others. Not that any of them were bad ideas — some just turned out to be a little more trouble than they were worth when I sat down and tried to turn the idea into code. I had visions of using Retina Icons and custom images for the rating symbol, but the code that I stole for handling the fractional rating values relied on the content property of the ::before pseudo-element. The value of the content property can only be text; icons, images, or any other HTML is not valid in that context and won’t get resolved. Basically, what I had in mind just wasn’t going to work.

That left me two choices: 1) redesign the HTML and CSS for the graphic to use something other than the content property, or 2) live with the restrictions and try to do what I wanted using unicode characters. I played around with choice #1 for quite a while, but I could never really come up with anything that gave me all of the flexibility that I wanted and still functioned correctly. So, I finally decided to see what was behind door #2. There are a lot of unicode characters. In fact, there are considerably more of those than there are Retina Icons, but not being able to use an image of your own choosing was still quite a bit more limiting than what I was imagining. Sill, it was better than just hard-coded starts, so I started hacking up the code to see if I could make it all work.

In my original version, the 5 stars were just an integral part of the rating CSS file:

.snh-rating::before {
    content: '★★★★★';
    background: linear-gradient(90deg, var(--star-background) var(--percent), var(--star-color) var(--percent));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

To make that more flexible, I just need to replace the hard-coded stars with a CSS variable:

.snh-rating::before {
    content: var(--char-content);
    background: linear-gradient(90deg, var(--star-background) var(--percent), var(--star-color) var(--percent));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

This would allow me to accept a new attribute as the unicode character to use and then set the value of that CSS variable to a string of those characters. My original Angular Provider had the template defined as a string, but to accommodate all of my various options, I had to convert that to a function. The first order of business in the function was to initialize all of the default values.

var max = 5;
var symbol = 'star';
var plural = 'stars';
var charCode = '★';
var charColor = '#FFCC00';
var subject = 'item';
var color = ['#F44336','#FF9800','#00BCD4','#2196F3','#4CAF50'];

Without overriding any of these defaults, the new version would produce the same results as the old version. But the whole point of this version was to provide the capability to override these values, so that was the code that had to be added next:

if (attrs.snhMax && parseInt(attrs.snhMax) > 0) {
	max = parseInt(attrs.snhMax);
}
if (attrs.snhSymbol) {
	symbol = attrs.snhSymbol;
	if (attrs.snhPlural) {
		plural = attrs.snhPlural;
	} else {
		plural = symbol + 's';
	}
}
if (attrs.snhChar) {
	charCode = attrs.snhChar;
}
var content = '';
if (attrs.snhChars) {
	content = attrs.snhChars;
} else {
	for (var i=0; i<max; i++) {
		content += charCode;
	}
}
if (attrs.snhCharColor) {
	charColor = attrs.snhCharColor;
}
if (attrs.snhSubject) {
	subject = attrs.snhSubject;
}
if (attrs.snhBarColors) {
	color = JSON.parse(attrs.snhBarColors);
}

The above code is pretty straightforward; if there are attributes present that override the defaults, then the default value is overwritten by the value of the attribute. Once that work has been done, all that is left is to build the template based on the variable values.

var htmlText = '';
htmlText += '<div>\n';
htmlText += '  <div ng-hide="votes > 0">\n';
htmlText += '    This item has not yet been rated.\n';
htmlText += '  </div>\n';
htmlText += '  <div ng-show="votes > 0">\n';
htmlText += '    <div class="snh-rating" style="--rating: {{average}}; --char-content: \'' + content + '\'; --char-color: ' + charColor + ';">';
htmlText += '</div>\n';
htmlText += '    <div style="clear: both;"></div>\n';
htmlText += '    {{average}} average based on {{votes}} reviews.\n';
htmlText += '    <a href="javascript:void(0);" ng-click="c.data.show_breakdown = 1;" ng-hide="c.data.show_breakdown == 1">Show breakdown</a>\n';
htmlText += '    <div ng-show="c.data.show_breakdown == 1" style="background-color: #ffffff; max-width: 500px; padding: 15px;">\n';
for (var x=max; x>0; x--) {
	var i = x - 1;
	htmlText += '      <div class="snh-rating-row">\n';
	htmlText += '        <div class="snh-rating-side">\n';
	htmlText += '          <div>' + x + ' ' + (x>1?plural:symbol) + '</div>\n';
	htmlText += '        </div>\n';
	htmlText += '        <div class="snh-rating-middle">\n';
	htmlText += '          <div class="snh-rating-bar-container">\n';
	htmlText += '            <div style="--bar-length: {{bar[' + i + ']}};--bar-color: ' + color[i] + ';" class="snh-rating-bar"></div>\n';
	htmlText += '          </div>\n';
	htmlText += '        </div>\n';
	htmlText += '        <div class="snh-rating-side snh-rating-right">\n';
	htmlText += '          <div>{{values[' + i + ']}}</div>\n';
	htmlText += '        </div>\n';
	htmlText += '      </div>\n';
}
htmlText += '      <div style="text-align: center;">\n';
htmlText += '        <a href="javascript:void(0);" ng-click="c.data.show_breakdown = 0;">Hide breakdown</a>\n';
htmlText += '      </div>\n';
htmlText += '    </div>\n';
htmlText += '  </div>\n';
htmlText += '</div>\n';

Now, all we have to do is try it out. Let’s configure a few of thee options to override the defaults and then see how it all comes out. Here is one sample configuration that uses 7 hearts instead of the default 5 stars:

  <snh-rating
     snh-max="7"
     snh-char="♥"
     snh-symbol="heart"
     snh-char-color="#FF0000"
     snh-values="'2,6,11,13,4,77,36'"
     snh-bar-colors='["#FFD3D3","#F4C2C2","#FF6961","#FF5C5C","#FF1C00","#FF0800","#FF0000"]'>
  </snh-rating>

… and here’s how it looks once everything is rendered:

Rating widget output with default values overridden

So, it’s not every single thing that I had imagined, but it is much more flexible than the original. Like most things, it could still be even better, but for now, I’m ready to call it good enough. If you want to play around with it on your own, here is an Update Set.

User Rating Scorecard, Part II

“Critics are our friends, they show us our faults.”
Benjamin Franklin

Now that I had a concept for displaying the results of collecting feedback, I just needed to build the Angular Provider to produce the desired output. I had already built a couple of other Angular Providers for my Form Field and User Help efforts, so I was a little familiar with the concept. Still, I learned quite a lot during this particular adventure.

To start with, I had never used variables in CSS before. In fact, I never really knew that you could even do something like that. I stumbled across the concept looking for a way to display partial stars in the rating graphic, and ended up using it in displaying the colored bars in the rating breakdown chart as well. For the rating graphic, here is the final version of the CSS that I ended up with:

:root {
	--star-size: x-large;
	--star-color: #ccc;
	--star-background: #fc0;
}

.snh-rating {
	--percent: calc(var(--rating) / 5 * 100%);
	display: inline-block;
	font-size: var(--star-size);
	font-family: Times;
	line-height: 1;
}
  
.snh-rating::before {
	content: '★★★★★';
	background: linear-gradient(90deg, var(--star-background) var(--percent), var(--star-color) var(--percent));
	-webkit-background-clip: text;
	-webkit-text-fill-color: transparent;
}

The portion of the HTML that produces the star rating came out to be this:

<div class="snh-rating" style="--rating: {{average}};"></div>

… and the average value was calculated by adding up all of the values and dividing by the number of votes:

$scope.valueString = $scope.$eval($attributes.snhValues);
$scope.values = $scope.valueString.split(',');
$scope.votes = 0;
$scope.total = 0;
for (var i=0; i<$scope.values.length; i++) {
	var votes = parseInt($scope.values[i]);
	$scope.votes += votes;
	$scope.total += votes * (i + 1);
}
$scope.average = ($scope.total/$scope.votes).toFixed(2);

The content is simply 5 star characters and then the linear-gradient background controls how much of the five stars are highlighted. The computed average score passed as a variable allows the script to dictate to the stylesheet the desired position of the end of the highlighted area. Pretty slick stuff, and this part I actually understand!

Once I figured all of that out, I was able to adapt the same concept to the graph that illustrated the breakdown of votes cast. In the case of the graph, I needed to find the largest vote count to set the scale of the graph, to which I added 10% padding so that even the largest bar wouldn’t go all the way across. To figure all of that out, I just needed to expand a little bit on the code above:

link: function ($scope, $element, $attributes) {
	$scope.valueString = $scope.$eval($attributes.snhValues);
	$scope.values = $scope.valueString.split(',');
	$scope.votes = 0;
	$scope.total = 0;
	var max = 0;
	for (var i=0; i<$scope.values.length; i++) {
		var votes = parseInt($scope.values[i]);
		$scope.votes += votes;
		$scope.total += votes * (i + 1);
		if (votes > max) {
			max = votes;
		}
	}
	$scope.bar = [];
	for (var i=0; i<$scope.values.length; i++) {
		$scope.bar[i] = (($scope.values[i] * 100) / (max * 1.1)) + '%';
	}
	$scope.average = ($scope.total/$scope.votes).toFixed(2);
},

The CSS to set the bar length then just needed to reference a variable:

.snh-rating-bar {
	width: var(--bar-length);
	height: 18px;
}

… and then the HTML for the bar just needed to pass in relevant value:

<div style="--bar-length: {{bar[0]}};" class="snh-rating-bar snh-rating-bar-1"></div>

All together, the entire Angular Provider came out like this:

function() {
	return {
		restrict: 'E',
		replace: true,
		link: function ($scope, $element, $attributes) {
			$scope.valueString = $scope.$eval($attributes.snhValues);
			$scope.values = $scope.valueString.split(',');
			$scope.votes = 0;
			$scope.total = 0;
			var max = 0;
			for (var i=0; i<$scope.values.length; i++) {
				var votes = parseInt($scope.values[i]);
				$scope.votes += votes;
				$scope.total += votes * (i + 1);
				if (votes > max) {
					max = votes;
				}
			}
			$scope.bar = [];
			for (var i=0; i<$scope.values.length; i++) {
				$scope.bar[i] = (($scope.values[i] * 100) / (max * 1.1)) + '%';
			}
			$scope.average = ($scope.total/$scope.votes).toFixed(2);
		},
		template: '<div>\n' +
			'  <div ng-hide="votes > 0">\n' +
			'    This item has not yet been rated.\n' +
			'  </div>\n' +
			'  <div ng-show="votes > 0">\n' +
			'    <div class="snh-rating" style="--rating: {{average}};"></div>\n' +
			'    <div style="clear: both;"></div>\n' +
			'    {{average}} average based on {{votes}} reviews.\n' +
			'    <a href="javascript:void(0);" ng-click="c.data.show_breakdown = 1;" ng-hide="c.data.show_breakdown == 1">Show breakdown</a>\n' +
			'    <div ng-show="c.data.show_breakdown == 1" style="background-color: #ffffff; max-width: 500px; padding: 15px;">\n' +
			'      <div class="snh-rating-row">\n' +
			'        <div class="snh-rating-side">\n' +
			'          <div>5 star</div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-middle">\n' +
			'          <div class="snh-rating-bar-container">\n' +
			'            <div style="--bar-length: {{bar[4]}};" class="snh-rating-bar snh-rating-bar-5"></div>\n' +
			'          </div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-side snh-rating-right">\n' +
			'          <div>{{values[4]}}</div>\n' +
			'        </div>\n' +
			'      </div>\n' +
			'      <div class="snh-rating-row">\n' +
			'        <div class="snh-rating-side">\n' +
			'          <div>4 star</div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-middle">\n' +
			'          <div class="snh-rating-bar-container">\n' +
			'            <div style="--bar-length: {{bar[3]}};" class="snh-rating-bar snh-rating-bar-4"></div>\n' +
			'          </div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-side snh-rating-right">\n' +
			'          <div>{{values[3]}}</div>\n' +
			'        </div>\n' +
			'      </div>\n' +
			'      <div class="snh-rating-row">\n' +
			'        <div class="snh-rating-side">\n' +
			'          <div>3 star</div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-middle">\n' +
			'          <div class="snh-rating-bar-container">\n' +
			'            <div style="--bar-length: {{bar[2]}};" class="snh-rating-bar snh-rating-bar-3"></div>\n' +
			'          </div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-side snh-rating-right">\n' +
			'          <div>{{values[2]}}</div>\n' +
			'        </div>\n' +
			'      </div>\n' +
			'      <div class="snh-rating-row">\n' +
			'        <div class="snh-rating-side">\n' +
			'          <div>2 star</div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-middle">\n' +
			'          <div class="snh-rating-bar-container">\n' +
			'            <div style="--bar-length: {{bar[1]}};" class="snh-rating-bar snh-rating-bar-2"></div>\n' +
			'          </div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-side snh-rating-right">\n' +
			'          <div>{{values[1]}}</div>\n' +
			'        </div>\n' +
			'      </div>\n' +
			'      <div class="snh-rating-row">\n' +
			'        <div class="snh-rating-side">\n' +
			'          <div>1 star</div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-middle">\n' +
			'          <div class="snh-rating-bar-container">\n' +
			'            <div style="--bar-length: {{bar[0]}};" class="snh-rating-bar snh-rating-bar-1"></div>\n' +
			'          </div>\n' +
			'        </div>\n' +
			'        <div class="snh-rating-side snh-rating-right">\n' +
			'          <div>{{values[0]}}</div>\n' +
			'        </div>\n' +
			'      </div>\n' +
			'      <div style="text-align: center;">\n' +
			'        <a href="javascript:void(0);" ng-click="c.data.show_breakdown = 0;">Hide breakdown</a>\n' +
			'      </div>\n' +
			'    </div>\n' + 
			'  </div>\n' + 
			'</div>\n'
	};
}

… and here is the accompanying CSS style sheet:

:root {
	--star-size: x-large;
	--star-color: #ccc;
	--star-background: #fc0;
}

.snh-rating {
	--percent: calc(var(--rating) / 5 * 100%);
	display: inline-block;
	font-size: var(--star-size);
	font-family: Times;
	line-height: 1;
}
  
.snh-rating::before {
	content: '★★★★★';
	background: linear-gradient(90deg, var(--star-background) var(--percent), var(--star-color) var(--percent));
	-webkit-background-clip: text;
	-webkit-text-fill-color: transparent;
}

* {
	box-sizing: border-box;
}

.snh-rating-side {
	float: left;
	width: 15%;
	margin-top: 10px;
}

.snh-rating-middle {
	margin-top: 10px;
	float: left;
	width: 70%;
}

.snh-rating-right {
	text-align: right;
}


.snh-rating-row:after {
	content: "";
	display: table;
	clear: both;
}

.snh-rating-bar-container {
	width: 100%;
	background-color: #f1f1f1;
	text-align: center;
	color: white;
}

.snh-rating-bar {
	width: var(--bar-length);
	height: 18px;
}

.snh-rating-bar-5 {
	background-color: #4CAF50;
}

.snh-rating-bar-4 {
	background-color: #2196F3;
}

.snh-rating-bar-3 {
	background-color: #00bcd4;
}

.snh-rating-bar-2 {
	background-color: #ff9800;
}

.snh-rating-bar-1 {
	background-color: #f44336;
}

@media (max-width: 400px) {
	.snh-rating-side, .snh-rating-middle {
		width: 100%;
	}
	.snh-rating-right {
		display: none;
	}
}

Now, I ended up hard-coding the number of stars, or possible rating points, throughout this entire exercise, which I am not necessarily all that proud of, but I did get it all to work. In my defense, the “5 Star” rating system seems to be almost universal, even if you aren’t dealing with “Stars” and are counting pizzas or happy faces. Hardly anyone uses 4 or 6 or 10 Whatevers to rate anything these days. Still, I would much prefer to be able to set both the number of items and the image for the item, just have a more flexible component. But then, this is just Version 1.0 … maybe one day some future version will actually have that capability. In the meantime, here is an Update Set for those of you who would like to tinker on your own.

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.

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.

Customizing the Data Table Widget, Part III

“Many of life’s failures are people who did not realize how close they were to success when they gave up.”
Thomas Alva Edison

In addition to the reference links that I wanted to add to my version of the Data Table widget, I also wanted to add the capability to add buttons or icons to each row for specific actions. This seemed like something that could be accomplished relatively easily, and configured just like the many other options associated with the various flavors the Data Table. My thought was to be able to configure things to look something like this:

Data Table with example button and action icon

For the icons, my plan was to just leverage the Retina Icons that I had stumbled across the other day, and then lift the associated HTML right out of the old snh-form-field tag. But first, I had to come up with a way to pass in all of the information needed to configure each button. There is a way to use a table as a source for complex widget options, but this was just an experiment at this point, so I decided to go the quick and easy way and just pass in a JSON array of button specification objects that would look something like this:

[
  {
    "name": "button",
    "label": "Button",
    "heading": "Button",
    "color": "primary",
    "hint": "Click this button to do something",
    "action": "doSomething"
  },{
    ... etc ...
  }
]

I haven’t worked out all of the details yet, but you get the idea. That’s the basic concept, anyway … all we have to do now is to code it up!

The first order of business is to create the new widget option to hold the button specifications. One quick way to do that is to just edit the Option Schema directly, as it just happens to be a JSON object itself. In fact, there is already one option defined (enable_filter), so it is a simple matter to just use that one as an example and create a second one for our purposes:

[
   {
      "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"
   }
]

Once we have updated the Option Schema, editing an instance of the widget will include a place to enter the specifications for any desired buttons. Now we have to add code to the widget’s Server Script to pull in the value of the new option and turn it from a String to an Object. Since there is no guarantee that the instance author will provide a parsable JSON object, we will have to put in a little defensive code to check for a few possibilities.

if (data.buttons) {
	try {
		var buttoninfo = JSON.parse(data.buttons);
		if (Array.isArray(buttoninfo)) {
			data.buttons = buttoninfo;
		} else if (typeof buttoninfo == 'object') {
			data.buttons = [];
			data.buttons[0] = buttoninfo;
		} else {
			gs.error('Invalid buttons option in SNH Data Table widget: ' + data.buttons);
			data.buttons = [];
		}
	} catch (e) {
		gs.error('Unparsable buttons option in SNH Data Table widget: ' + data.buttons);
		data.buttons = [];
	}
} else {
	data.buttons = [];
}

And finally, we have to alter the HTML to include any configured buttons or icons. Modifications will need to be made in two places, one for the column headings and the other for the data columns. Here is what we will add for the column headings:

<th ng-repeat="button in data.buttons" class="text-nowrap center" tabindex="0">
  {{button.heading || button.label}}
</th>

And this is what we will toss in for each row of data:

<td ng-repeat="button in data.buttons" class="text-nowrap center" 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>

That puts everything on the screen and clickable, but we still some code to perform whatever action each button is intended to perform. That’s a tad bit more complicated, so I think we’ll tackle that the next time out ….

Customizing the Data Table Widget, Part II

“It always seems impossible until it’s done.”
Nelson Mandela

Today I am going continue on with my little Data Table widget customization by tackling the HTML portion of project, and then set things up so that we can do a little testing. Currently, the HTML that displays a single row in the data table does so in a way that the entire row is the clickable link to the details for that row. My intent is to change that so that the first column is the link to the details for that row, and then any other column that contains a reference field can be a link to the details of that specific reference. So, let’s take a look at the HTML structure that is there now:

<tbody>
  <tr ng-repeat="item in data.list track by item.sys_id">
    <td class="sr-only" tabindex="0" role="link" ng-click="go(item.targetTable, item)" aria-label="${Open record}"></td>
    <td aria-label="{{item[field].display_value}}" class="pointer sp-list-cell" ng-class="{selected: item.selected}" ng-click="go(item.targetTable, item)" ng-repeat="field in ::data.fields_array" data-field="{{::field}}" data-th="{{::data.column_labels[field]}}">{{::item[field].display_value}}</td>
  </tr>
</tbody>

I’m not quite sure what purpose is served by that first <TD> that doesn’t repeat, but there is a column heading to match, although neither appear to have any value or content. Maybe it’s a spot for a row-level checkbox or a glyph or something, but I’m not really smart enough to figure it out. The first thing that I did on my version was to delete it, along with the associated column heading — if I don’t understand why it needs to be there, then it just needs to go. Of course, that has nothing to do with what I am trying to accomplish, but if you happen to notice it gone in my version, that’s because I made it gone. I may end up having to go out and look for it one day when I finally figure out its value, but for now at least, it sleeps with the fishes.

With that out of the way, we can focus on the actual table columns, which are all treated the same in this version. The entire cell, not just the content, is the link to further information on the row, and with every column the same, no matter where you click on the row, the result is the same. We don’t want to do that in our version, though, so we can keep the ng-repeat on the <TD>, but the rest will be dependent on the column. Since we want the links to be on the content and not on the entire cell, we don’t really need anything else at the <TD> level anyway.

The first column will always be a link, and it will perform the same function as clicking anywhere on the row in the original version. For all other columns, they will also be links if the data type is reference, and there is a value. We can use the $first property to detect the first column, so the code for that link can basically be lifted from the original, with the addition of an ng-if to target the first column.

<a ng-if="$first" href="javascript:void(0)" ng-click="go(item.targetTable, item)" title="${Click for more on }{{::item[field].display_value}}">{{::item[field].display_value}}</a>

The remaining columns will require a few more lines. Not only do we need to determine that this not the first column, we also need to check to see of the type is reference, and if there is a value (if there is no value, then there is no need to code the link). Wrapping the whole thing in a <SPAN> will allow us to separate all of this from the first column, and then we can use an anchor tag for the links and a simple <SPAN> for the rest.

<span ng-if="!$first">
  <a ng-if="item[field].value && item[field].type == 'reference'" href="javascript:void(0)" ng-click="go(item[field].table, item[field].record)" title="${Click for more on }{{::item[field].display_value}}">{{::item[field].display_value}}</a>
  <span ng-if="!item[field].value || item[field].type != 'reference'">{{::item[field].display_value}}</span>
</span>

At this point, I am leveraging the existing go function to handle the clicks. That function takes a table and a record as an argument, so I figure that I can just pass in the name of the referenced table and a mocked-up record (more on that later) and simply reuse the existing function. I may want to rethink that later and create a function specifically for reference field clicks, but for now this works, so it’s good enough. My earlier server-side additions did not originally create mocked-up record for each reference field, so I had to go back and that in to bring all of this together. I added that to the same block of code where I was adding the name of related table.

if (record[fld].type == 'reference') {
	record[fld].table = gr.getElement(fld).getED().getReference();
	record[fld].record = {sys_id: {value: record[fld].value, display_value: record[fld].value}, name: {value: record[fld].display_value, display_value: record[fld].display_value}};
}

Well, that should do it — at least, for this initial version. Now we just need to take it out for a spin and make sure that everything works. For that, we’ll need to create a test page and then put the widget on the page and configure it. Then we can hit the Try It! button.

First test of the customized Data Table widget

Well, that wasn’t too bad. Everything seems to work as I had intended. Nothing happens right now when you click on anything, but that’s because the go function simply broadcasts the click events, and I don’t have anything listening for that in this version. This was a clone of the core Data Table widget, and the listeners are in the wrapper widgets that implement the various ways to configure the display and contents. I’ll have to clone those wrapper widgets as well, or maybe modify them to choose between the stock Data Table widget and my customized version. Either one seems like a lot more work than I want to dive into right now, so I’m going to leave that for a later installment.