Service Portal Form Fields, Broken

“Well, if it can be thought, it can be done, a problem can be overcome.”
E.A. Bucchianeri

The snh-form-field tag has been around for a while in various incarnations, and has been used on quite a few projects for one thing or another, all with relative success. Several snh-form-fields make up the initial set-up screen for the Collaboration Store, which also worked out quite well for that particular effort. At least, it was working out quite well until I tried to set up a new instance that was running the new Tokyo release. Here is what the initial set-up screen is supposed to look like:

Collaboration Store initial set-up screen

Here is what it looks like on a Tokyo instance:

Collaboration Store initial set-up screen on Tokyo

As you can clearly see, a couple of very important input fields are not on the screen in the Tokyo version. This appears to be due to a flaw in the snh-form-field tag that did not reveal itself in the earlier versions of the Now Platform. At this point, I do not know the exact nature of the flaw, only that it appears that you can only have one snh-form-field tag in any given container. I would categorize this as a flaw in the tag rather than an issue with the new release of ServiceNow, mainly because of the first rule of programming. It may take me a while to figure out exactly what might be going on here, but in the interim, I was at least able to come up with a viable work around. If you are using the snh-form-field tag on a project running on a Tokyo instance and you run into this issue before the flaw in the tag has been corrected, you can simply surround each snh-form-field tag with a span. That places each snh-form-field tag in its own personal container, eliminating the issue.

Here is how I used this technique to work around the problem that I was having with the initial set-up screen for the Collaboration Store.

<div id="nav_message" class="outputmsg_nav" ng-hide="c.data.validScope">
  <img role="presentation" src="images/icon_nav_info.png">
  <span class="outputmsg_nav_inner">
    &nbsp;
    The <strong>Collaboration Store Set-up</strong> cannot be completed because <strong>Collaboration Store</strong> is not selected in your application picker.
    <a onclick="window.location.href='change_current_app.do?app_id=5b9c19242f6030104425fcecf699b6ec&referrer=%24sp.do%3Fid%3Dcollaboration_store_setup'">
      Switch to <strong>Collaboration Store</strong>
    </a>.
  </span>
</div>
<div class="row" ng-show="c.data.phase != 1 && c.data.phase != 2 && c.data.phase != 3">
  <div class="col-sm-12">
    <h4>
      <i class="fa fa-spinner fa-spin"></i>
      ${Wait for it ...}
    </h4>
  </div>
</div>
<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 and the Host instance will need to be
      available to complete the set-up process.
    </p>
  </div>
  <form id="form1" name="form1" novalidate>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <span>
          <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"}]'/>
        </span>
        <span>
          <snh-form-field
            snh-model="c.data.host_instance_id"
            snh-name="host_instance_id"
            snh-label="${Host Instance ID}"
            snh-help="Enter the instance ID only, not the full URL of the instance (https://{instance_id}.servicenow.com))"
            snh-required="c.data.instance_type == 'client'"
            ng-show="c.data.instance_type == 'client'"/>
        </span>
        <span>
          <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'"/>
        </span>
        <span>
          <snh-form-field
            snh-model="c.data.instance_name"
            snh-name="instance_name"
            snh-label="${Instance Label}"
            snh-required="true"/>
        </span>
      </div>
      <div class="col-xs-12 col-sm-6 text-center">
        <div class="row" style="padding: 15px;">
          <div class="avatar-extra-large avatar-container" style="cursor:default;">
            <div class="avatar soloAvatar bottom">
              <div class="sub-avatar mia" ng-style="instanceLogoImage"><i class="fa fa-image"></i></div>
            </div>
          </div>
        </div>
        <div class="row" style="padding: 15px;">
          <input ng-show="false" type="file" accept="image/jpeg,image/png,image/bmp,image/x-windows-bmp,image/gif,image/x-icon,image/svg+xml" ng-file-select="attachFiles({files: $files})" />
          <button ng-click="uploadInstanceLogoImage($event)"
                  ng-keypress="uploadInstanceLogoImage($event)" type="button"
                  class="btn btn-default send-message">${Upload Instance Logo Image}</button>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-12">
        <span>
          <snh-form-field
            snh-model="c.data.email"
            snh-name="email"
            snh-label="${Email}"
            snh-help="A verification email will be sent to this address as part of the set-up process"
            snh-type="email"
            snh-required="true"/>
        </span>
        <span>
          <snh-form-field
            snh-model="c.data.description"
            snh-name="description"
            snh-label="${Instance Description}"
            snh-type="textarea"
            snh-required="true"/>
        </span>
      </div>
    </div>
  </form>
  <div class="row">
    <div class="col-sm-12" style="text-align: center;">
      <button class="btn btn-primary" ng-disabled="!(form1.$valid) || !c.data.validScope" ng-show="c.data.instance_type == 'host'" ng-click="save();">${Create New Collaboration Store}</button>
      <button class="btn btn-primary" ng-disabled="!(form1.$valid) || !c.data.validScope" ng-show="c.data.instance_type == 'client'" ng-click="save();">${Complete Set-up and Request Access}</button>
    </div>
  </div>
</div>
<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">
        <span>
          <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"/>
        </span>
      </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>
<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 ng-if="c.data.instance_type == 'client'">
      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>
    <p ng-if="c.data.instance_type == 'host'">
      The Collaboration Store set-up is now complete. Your instance has been successfully set up as a
      Host instance and is now ready to accept client registrations and utilize the Collaboration
      Store features.
    </p>
  </div>
</div>

It’s not the best, but it does work. If you are one of those kind souls helping out with the testing of the Collaboration Store, and you happen to be doing your testing on a Tokyo instance, you can just grab the code above and overlay the HTML for the set-up widget with this version. That should get you up and running again. This will definitely be included in the next early release for testing purposes, but if you don’t want to wait for that, just snag the code above.

The best solution, of course, is to continue digging until we find the source of the true problem here and release a new version of SNH Form Fields that does not contain this issue. Hopefully, it won’t be too long before we can make that happen.

Flow Variables vs. Scratchpad Variables, Revisited

“Every great mistake has a halfway moment, a split second when it can be recalled and perhaps remedied.”
Pearl S. Buck

A while back, I took a quick look at the new Flow Variables feature of the Flow Designer in the hopes that the new feature might eliminate the need for my earlier effort to develop a Flow Designer Scratchpad. Unfortunately, I wasn’t able to figure out how to make it work in the way that it was described in the Blog Entry that I was attempting to follow. I tried a few things on my own, but that did not work for me, either. Since I was not smart enough to figure out how to make it do what I was trying to do, I just gave up.

Since that time, though, I have upgraded my Personal Developer Instance to Rome, so I thought that it might be a good time to go back and see if anything had changed since the last time that I tried it. As it turns out, not much has changed, but during my second look I realized that if I had just kept reading the original Blog Entry that I had been following, I would have come across the way to actually pull this off. The secret is in this little icon that I never bothered to click on when I was looking at things the first time.

Flow variable transform function icon

I still cannot simply add the +1 after the Flow Variable pill as it shows earlier in the post, but by clicking on that little icon, I can bring up the transform options and select Math -> Add from the Suggested Transforms menu. That brings up a brief dialog box where I can enter the 1 as the number that I would like to add to the current value.

Transform pop-up dialog

Unlike my earlier attempt at manually scripting the math, this actually worked when I ran a test execution.

Sample Flow test results

This is great news. Now all that I have to do is hunt down all of the places where I have used Scratchpad variables and see if I can replace them with Flow Variables. One of the Actions that I created using the Scratchpad was the Array Iterator, which was built back when the For Each Flow Logic only supported GlideRecords. Now that For Each can be used on Arrays as well, there is no longer a need for the custom built Array Iterator. I also used the Scratchpad for a Counter, something else that could now be replaced with a Flow Variable (now that I know how to make it work!). I should really hunt down the entire lot and replace them all.

That sounds like a lot of work, though, so I will probably take that that on as I crack things open for other maintenance. Still, it is good to finally be able to use the features of the product rather than these homemade concoctions. They definitely served their purpose at the time, but now that the platform has caught up with my needs, it’s definitely time to move on.

sn-record-picker Helper, Revisited

“The only thing that should surprise us is that there are still some things that can surprise us.”
Francois de La Rochefoucauld

I’m not generally one to obsess over SEO or site rankings or any kind of traffic analysis, but occasionally, I will poke around and see what comes up on searches, just for my own amusement. For the most part, this whole web site is for my own amusement, and I don’t really expend a lot energy trying to obtain an audience or generate any kind of interest. Mainly, I just like to create stuff or see what I can do, and then share it with anyone who might have a similar interest. So it surprises me quite a bit to search the web for something and find this site listed twice in the top 5 search results and also be the source of the definitive answer to the top question in the People also ask section of the results:

Google search results for sn record picker servicenow

There are quite a number of web sites out there that discuss this topic, so that just seems pretty amazing to me, all things considered. Let’s face it, my little tidbits of amateur hackery are pretty insignificant compared to the massive volumes of information out there dedicated to the ServiceNow product. I guess I should pay more attention, but I just did not expect to see that.

The other question that this brought to my mind was, why the sn-record-picker Helper? I mean, that was well over a year ago, and there are quite a few other areas of ServiceNow explored on this site. That was basically just a short two-part series, not counting the later correction. The sn-record-picker (also written and searched on as snRecordPicker, although that returns different results) is definitely a Service Portal thing, but I have written about a number of other Service Portal elements such as customizing the data table, dynamic breadcrumbs, integrated help, my delegates widget, my content selector and associated configurator, Highcharts, @mentions, a user directory, and my personal favorite, the snh-form-fields. Outside of the Service Portal, you can find other ServiceNow elements here such as the REST API, the Excel Parser, the Flow Designer, Webhooks, System Properties, Event Management, and a host of other just-as-interesting(?) topics. So what was so special about the sn-record-picker? I have no idea.

Still, it is interesting stuff. At least, it was interesting to me. One day I may invest a little more time into figuring out what drives such things, but other than finding it rather amazing when I stumble across it, I don’t see myself investing a whole lot of time on trying to manipulate or manufacture such results. It’s much more fun to me to build stuff and throw out Update Sets for people to pull down and play around with. Plus, if you have to manipulate the results artificially, then your place in the results is more of a reflection of your manipulation skills rather than the value of the content. I could build an article like this one that repeats the words sn-record-picker and snRecordPicker over and over again, just to see what happens, but no one wants to see that. At least, I don’t think so. Personally, I think my time would be much better spent getting back to building stuff, which is what I like to do anyway!

But there is a certain amount of satisfaction in seeing your own site show up on such a list. These things have a way of constantly shifting over time, though, so we’ll see how long it lasts.

Content Selector Configuration Editor, Corrected

“Beware of little expenses; a small leak will sink a great ship.”
Benjamin Franklin

It just seems to be the natural order of things that just when I think I finally wrapped something up and tied a big bow on it, I stumble across yet another fly in the ointment. The other day I was building yet another configuration script using my Content Selector Configuration Editor, but when I tried to use it, I ran into a problem. Since I happened to be working in a Scoped Application, the generated script failed to locate the base class, since that class in the global scope. The problem was this generated line of script:

ScopedAppConfig.prototype = Object.extendsObject(ContentSelectorConfig, {

For a Scoped Application, that line should really be this:

ScopedAppConfig.prototype = Object.extendsObject(global.ContentSelectorConfig, {

It seemed simple enough to fix, but first I had to figure out how to tell if I was in a Scoped App or not. After searching around for a bit, I finally came across this little useful tidbit:

var currentScope = gs.getCurrentApplicationScope();

Once I had a way of figuring out what scope I was in, it was easy enough to change this line:

script += ".prototype = Object.extendsObject(ContentSelectorConfig, {\n";

… to this:

script += ".prototype = Object.extendsObject(";
if (gs.getCurrentApplicationScope() != 'global') {
	script += "global.";			
}
script += "ContentSelectorConfig, {\n";

Once I made the change, I pulled my Scoped Application script up in the Content Selector Configuration Editor, saved it, and tried it again. This time, it worked, which was great. Unfortunately, later on when I went to pull it up in the editor again to make some additional changes, it wasn’t on the drop-down list. It didn’t take long to figure out that the problem was the database table search filter specified in the pick list form field definition:

<snh-form-field
  snh-label="Content Selector Configuration"
  snh-model="c.data.script"
  snh-name="script"
  snh-type="reference"
  snh-help="Select the Content Selector Configuration that you would like to edit."
  snh-change="scriptSelected();"
  placeholder="Choose a Content Selector Configuration"
  table="'sys_script_include'"
  default-query="'active=true^scriptCONTAINSObject.extendsObject(ContentSelectorConfig'"
  display-field="'name'"
  search-fields="'name'"
  value-field="'api_name'"/>

The current filter picks up all of the scripts tht contain “Object.extendsObject(ContentSelectorConfig”, but not those that contained “Object.extendsObject(global.ContentSelectorConfig”. I needed to tweak the filter just a bit to pick up both. I ended up with the following filter value:

active=true^scriptCONTAINSObject.extendsObject(ContentSelectorConfig^ORscriptCONTAINSObject.extendsObject(global.ContentSelectorConfig

… and that took care of that little issue. I’m sure that this will not be the last time that I run into a little issue with this particular product, but for now, here is yet another version stuffed into yet another Update Set. Hopefully, that will be it for the needed corrections for at least a little while!

Flow Variables vs. Scratchpad Variables

“An essential part of being a humble programmer is realizing that whenever there’s a problem with the code you’ve written, it’s always your fault.”
Jeff Atwood

Now that you can get yourself a copy of the new Quebec version of ServiceNow, you can start playing around with all of the new features that will be coming out with this release. One of those new features is Flow Variables, which look like they might serve to replace the Scratchpad that I had built earlier for pretty much the exact same purpose. Since I have used that custom feature in a variety of different places after I first put it together, the trick will be to see if I can just do a one-for-one swap without having to re-engineer things using an entirely different philosophy.

I like building parts, but I also like to get rid my home-made parts whenever I find that there is a ServiceNow component that can serve the same purpose. If I can figure out how to make a Flow Variable do all of the things that I have been able to do with a Scratchpad Variable, then I will be the first in line to throw my home-made scratchpad into the dust bin. But before we get too far ahead of ourselves, let’s try a few things and see what we can actually do.

According to the ServiceNow Developer Blog, you can set the value of a Flow Variable inside of a loop and use it to control the number of times that you go through the loop.

Subtracting 1 from an Integer Flow Variable

Let’s create a sample flow, define our first Integer Flow Variable and then set up a loop that we can escape based on the value of the variable. Open up the Flow Designer, click on the New button, select Flow from the drop-down, and then name the new Flow Sample Flow. From the vertical ellipses menu in the upper right-hand corner, we can select Flow Variables and define a new variable called index of type Integer. For our first step, we can set the value of our new variable to 0, and then we can add a loop that will run until the variable value is greater than 5. After that, all we will have to add is another step inside the loop to increment our index variable.

Simple variable test flow

All of that went relatively smoothly for me until I tried to add the +1 to the Set Flow Variables step inside of the loop. Once I dragged the index pill into the value box, it would not accept any more input. I tried putting the +1 in first, and then dragging the pill in front of it second, but that just replaced the +1. I could never get the pill+1 value like the sample image in the developers blog entry. Obviously, I was doing something wrong, but I did not know exactly what. Nothing that I tried seemed to work, though, so eventually I decided to give that up and just script it.

Scripted variable increment

It was a fairly simple script that just added one to the existing value.

return fd_data.flow_var.index + 1;

Unfortunately, that resulted in a text concatenation rather than an increment of the value. 0 + 1 became 01 and 01 + 1 became 011 and so forth. Even though I had defined my variable as an Integer, it was getting treated as if it were text. No problem, though … I will just convert it to an Integer before I attempt to increment the value.

return parseInt(fd_data.flow_var.index) + 1;

That’s better. Now 0 + 1 becomes 1 and 1 + 1 becomes 2 just the way that I thought that it should. But … things are still not working the way that they should. In my test runs, the new value was not always present when I reentered the loop. 0 + 1 became 1, but then the next time through, the variable value was still 0. That’s not right! I kept erroring out by looping through the code more than the maximum 10,000 times until I finally changed my bail-out condition to be any value greater than zero. Even then, it ended up looping 456 times before it dropped out.

Completed test run results

Clearly, something is not quite right here. It is possible that I just got a hold of an early release that still has a few kinks left to be worked out, but if you’re a true believer in The First Rule of Programming, then you know that I’ve just done something wrong somewhere along the line and need to figure out what that is. Obviously, you shouldn’t have to run through the loop over 400 times just bump the count above zero.

Well, back to the drawing board, as they say. Maybe I will have to resort to reading the instructions. Nah … I’ll just keep trying stuff until I get something to work. In the meantime, it looks like I won’t be flushing the old home-grown scratchpad down the drain just yet. At least not until I figure out how to make this thing work. Too bad, though. I had such high hopes when I saw that this was coming out.

Note: Eventually, I was able to figure out what I was doing wrong.

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.

Generic Feedback Widget, Part IV

“For all sad words of tongue and pen, the saddest are these, ‘It might have been.'”
John Greenleaf Whittier

Well, it seemed like a good idea at the time. In fact, I was pretty proud of my Generic Feedback Widget once I had it pretty much all put together. I even felt so good about it that I went ahead and put out an Update Set. Then I started playing around with it while using other User accounts that did not have the admin role, and I realized that something was seriously wrong. In fact, nothing really worked at all. If you are not an admin or an existing member of a conversation, not only can you not enter any new feedback; you can’t even see the existing feedback that is already there. That’s not right!

It took be a little digging around to finally lay my hands on the source of the problem, but I found it. There is read ACL on the live_group_profile table that includes the following script:

var gr = new GlideRecord('live_group_member');
gr.addQuery('member', GlideappLiveProfile().getID());
gr.addQuery('group', current.sys_id);
gr.addQuery('state', 'admin').addOrCondition('state', 'active');
gr.query();
answer = gr.next();

The impact of that ACL is that you cannot read a record from the live_group_profile table unless you are an existing member of that group. Without access to the group profile, you cannot obtain the sys_id of the group to use in the query of the live_feed_message table to see all of the messages. And you cannot put yourself in the group if you can’t get the ID of the group to include on the live_group_member record you would need to create in order to make yourself a member. The bottom line to all of that is that, if you are not an admin (which overrides this ACL), you cannot see any messages related to the subject of the page and you cannot create any. That pretty much kills the entire basis of what I was trying to do.

The question now, is what, if anything, can be done about it. Obviously, I could simply deactivate that ACL and the problem would be solved, but that would also open up all kinds of other problems that that ACL was designed to avoid, so that’s not really a viable option. I could give up on my desire to leverage these existing tables and functions and just set up all new tables for this process with their own ACLs, but that seems like quite a bit more work than I normally care to undertake. Still, it seems as though there has got to be a way to leverage what I have already built without breaking things that are already in the product and not signing up for a major project. I need to figure out a way for non group members to read the group record without effectively killing that ACL for other purposes, or I am going to have to start all over with custom tables of my own design.

This should be interesting …

Static Monthly Calendar, Part III

“Mistakes are the portals of discovery.”
James Joyce

While experimenting with a number of various configurations for my Static Monthly Calendar, I ran into a number of issue that led me to make a few adjustments to the code, and eventually, to actually build a few new parts that I am hoping might come in handy in some future effort. The first problem that I ran into was when I tried to configure a content provider from a scoped app. The code that I was using to instantiate a content provider using the name was this:

var ClassFromString = this[options.content_provider];
contentProvider = new ClassFromString();

This works great for a global Script Include, but for a scoped component, you end up with this:

var ClassFromString = this['my_scope.MyScriptInclude'];

… when what you really need is this:

var ClassFromString = this['my_scope']['MyScriptInclude'];

I started to fix that by adding code to the widget, but then I decided that it was code that would probably be useful in other circumstances, so I ended up creating a separate global component to turn an object name into an instance of that object. That code turned out to look like this:

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

	_root: null,

	setRoot: function(root) {
		this._root = root;
	},

	getInstance: function(name) {
		var instance;

		var scope;
		var parts = name.split('.');
		if (parts.length == 2) {
			scope = parts[0];
			name = parts[1];
		}
		var ClassFromString;
		try {
			if (scope) {
				ClassFromString = this._root[scope][name];
			} else {
				ClassFromString = this._root[name];
			}
			instance = new ClassFromString();
		} catch(e) {
			gs.error('Unable to instantiate instance named "' + name + '": ' + e);
		}

		return instance;
	},

	type: 'Instantiator'
};

This handles both global and scoped components, and also simplified the code in the widget, which turned out to be just this:

contentProvider = instantiator.getInstance(options.content_provider);

Another issue that I ran into was when I tried to inject content that allowed the user to click on an event to bring up some additional details about the event in a modal pop-up. I created a function called showDetails to handle the modal pop-up, and then added an ng-click to the enclosing DIV of the HTML provided by my example content provider call this new function. Unfortunately, the ng-click, which was added to the page with the rest of the provided content, was inserted using an ng-bind-html attribute, which simply copies in the raw HTML and doesn’t actually compile the AngularJS code. I tried various approaches to compiling the code myself, but I was never able to get any of those to work. Then I came across this, which seemed like just the thing that I needed. I thought about installing in in my instance, but then I thought that I had better check first, because it’s entirely possible that it is already in there. Sure enough, I came across the Angular Provider scBindHtmlCompile, which seemed like a version of the very same thing. So I attached it to my widget and replaced by ng-bind-html with sc-bind-html-compile.

Unfortunately, that just put the compiler into an endless loop, which ultimately resulted in filling up the Javascript console with quite a few of these error messages:

Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!

I searched around for a solution to that problem, but nothing that I tried would get around the problem. I ended up going in the opposite direction and swapping out the ng-click for an onclick, which doesn’t need to be compiled. Of course, the onclick can’t see any of the functions inside the scope of the app, so I had to write a stand-alone UI Script to include with a script tag in order to have a function to call. That function is outside of the scope of the app as well, so I ended up turning the script into yet another generic part that uses the element to get you back to the widget:

function functionBroker(id, func, arg1, arg2, arg3, arg4) {
	var scope = angular.element(document.getElementById(id)).scope();
	scope.$apply(function() {
		scope[func](arg1, arg2, arg3, arg4);
	});
}

You pass it the ID of your HTML element, the name of function that is in scope, and up to four independent arguments that you would like to pass to the function. It uses the element to locate the scope, and then uses the scope to find your desired function and passes in the arguments. After saving the new generic script, I went back into the widget and added a script tag to the widget’s HTML to pull the script onto the page.

<script type="text/javascript" src="/function_broker.jsdbx"></script>

Then I added a function to pop open a modal dialog based on a configuration object passed into the function.

$scope.showDetails = function(modalConfig) {
	spModal.open(modalConfig);
};

Now, I just needed something to pop up to see if it all worked. Not too long ago I made a simple widget to show off my rating type form field, and that looked like a good candidate to use just to see if everything was going to work out the way that it should. I pulled up the ExampleContentProvider that I created earlier, and added one more event in the middle of the month that would bring up this repurposed widget when clicked.

if (dd == 15) {
	response += '<div class="event" id="event15" onclick="functionBroker(\'event15\', \'showDetails\', {title: \'Fifteenth of the Month Celebration\', widget:\'feedback-example\', size: \'lg\'});" style="cursor: pointer;">\n';
	response += '  <div class="event-desc">\n';
	response += '    Fifteenth of the Month Celebration\n';
	response += '  </div>\n';
	response += '  <div class="event-time">\n';
	response += '    Party Time\n';
	response += '  </div>\n';
	response += '</div>\n';

The whole thing is kind of a Rube Goldberg operation, but it should work, so let’s light things up and give it a try.

Modal pop-up from clicking on an Event

After all of the failed attempts at making this happen, it’s nice to see the modal dialog actually appear on the screen! It still seems like there has got to be a simpler way to make this work, but until I figure that out, this will do. If you’d like to play around with it yourself, here’s an Update Set that I hope includes all of the right pieces. There are still a few little things that I would like to add one day, so this may not quite be the last you will see of this one.

Portal Widgets on UI Pages, Revisited

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

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

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

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

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

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

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

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

Dynamic Service Portal Breadcrumbs, Corrected

“Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.”
Linus Torvalds

While I was playing around with my workload chart, I noticed a little bug in my dynamic breadcrumbs widget: returning to the Home page does not reset the breadcrumbs for a new trail out from the home page. Instead, all of the previous trail of pages remains intact. This is not the way that this is supposed to work. Once you return to the home page, the trail should start over. At first, I never noticed this, because I never added the breadcrumbs widget to the home page. Without the breadcrumbs widget on the home page, I wouldn’t expect it to reset. But once I did that and it still didn’t work, I had to get busy trying to figure out why that was.

The breadcrumbs data that was left behind on the previous page is retrieved from the User Preference on the server side, and then the new breadcrumbs data is established on the client side. Initially, the breadcrumbs data is set to an empty Array, and then if we are not on the home page, we push all of the existing pages onto the array until we come across the current page or run out of existing pages. In the case of the home page, the empty array is all that remains. Here is the relevant code:

c.breadcrumbs = [];
var thisPage = {url: $location.url(), id: $location.search()['id'], label: c.data.page || document.title};
if (thisPage.id != $rootScope.portal.homepage_dv) {
	var pageFound = false;
	for (var i=0;i<c.data.breadcrumbs.length && !pageFound; i++) {
		if (c.data.breadcrumbs[i].id == thisPage.id) {
			c.breadcrumbs.push(thisPage);
			pageFound = true;
		} else {
			c.breadcrumbs.push(c.data.breadcrumbs[i]);
		}
	}
	if (!pageFound) {
		c.breadcrumbs.push(thisPage);
	}
}
c.data.breadcrumbs = c.breadcrumbs;
c.server.update();

On the server side of things, my intent was to save the breadcrumbs once established, and here is the code that was supposed to handle that:

if (input.breadcrumbs) {
	gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
}

My thinking was that, if the breadcrumbs were established on the client side, then save them to the User Preference. Unfortunately, the simple conditional input.breadcrumbs returns false for an empty array, not true as I had assumed (hey, an empty array is still something and not null!); therefore, the saving of the breadcrumbs was not executed when on the home page. I should have know that, I guess, but I’m no longer young enough to know everything. I still get to learn something new every day. Once I figured that out, I changed it to this:

if (Array.isArray(input.breadcrumbs)) {
	gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
}

A simple change, but one that made it work the way that I had intended instead of the way that I had coded it. That took care of that little issue. I included the updated breadcrumbs widget in the last Update Set for my Highcharts example, but for those of you who are only interested in the breadcrumbs widget, I created a separate Update Set, which you can grab here.

Update: There is an even better version, which you can find here.