Content Selector Configuration Editor, Part III

“For every complex problem there is an answer that is clear, simple, and wrong.”
H. L. Mencken

Now that we have completed all of the coding on the main widget, it is time to build the new Perspective Editor widget that we will launch in the modal dialog. This will be a simple form with three input fields for the three properties of a Perspective: Label, Name, and Roles. The first two will be simple text fields, but we can select the Roles from a list, so for that we can leverage our old friend, the sn-record-picker. Once again, we can start out with the HTML, just to see how things look.

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="persp"
      snh-label="Name"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.roles"
      snh-name="roles"
      snh-type="reference"
      table="'sys_user_role'"
      field="c.widget.options.shared.roles"
      default-query="'active=true'"
      display-field="'name'"
      search-fields="'name'"
      value-field="'name'"
      multiple="true"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

Basically, this is just three standard snh-form-field elements, the first two being of type text and the last one being of type reference, which is just a wrapper around the sn-record-picker. There are a couple of things to note here: 1) when I tried to use the word name for the name of the name field, it crashed the widget, so I called it persp instead, and 2) I ended up adding a couple of buttons to the layout, even though the spModal already provides buttons for you (more on that later). I’m not sure why using the word name crashed the widget, but I have a sinking feeling that there is some kind of bug in the snh-form-field code. I didn’t really feel like digging into that right at the moment, though, and changing the name fixed the problem, so I’m good for now. Here’s how it looks when it gets launched:

Perspective Editor layout

Now, about those buttons: if you don’t override the defaults, an spModal pop-up will have two standard buttons, Cancel and OK. I wanted to use those buttons, but I also wanted to validate the form, and I’m not smart enough to know how to insert form validation underneath the OK button so that it won’t just go right back to the main page and close the pop-up. I tried a few things, but nothing worked, so I ended up adding my own buttons, hiding the originals, but still clicking on them programmatically to obtain their original function. It’s pretty much a convoluted hack, but it gets the job done, so I’ll take it. Here is the client-side code that makes all of that work:

function PerspectiveEditor($scope, $timeout) {
	var c = this;

	$scope.cancel = function() {
		$timeout(function() {
			angular.element('[ng-click*="buttonClicked"]').get(0).click(); 
		});
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			$timeout(function() {
				angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	$timeout(function() {
		angular.element('[class*="modal-footer"]').css({display:'none'});
	}, 100);
}

That’s it. There is no server-side code on this one and no link code, so that’s the entire widget. That completes everything for the Perspectives section, and now that it all checks out, we can pretty much just copy it all to create the States section. Let’s start with the HTML:

<div>
  <h4 class="text-primary">${States}</h4>
</div>
<div>
  <table class="table table-hover table-condensed">
    <thead>
      <tr>
        <th style="text-align: center;">Label</th>
        <th style="text-align: center;">Name</th>
        <th style="text-align: center;">Edit</th>
        <th style="text-align: center;">Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in c.data.config.state" ng-hide="item.removed">
        <td data-th="Name">{{item.label}}</td>
        <td data-th="Label">{{item.name}}</td>
        <td data-th="Edit" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editState($index)" alt="Click here to edit the details of this State" title="Click here to edit the details of this State" style="cursor: pointer;"/></td>
        <td data-th="Delete" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteState($index)" alt="Click here to permanently delete this State" title="Click here to permanently delete this State" style="cursor: pointer;"/></td>
      </tr>
    </tbody>
  </table>
</div>
<div style="width: 100%; text-align: right;">
  <button ng-click="editState('new')" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to add a new State">Add a new State</button>
</div>

This is just a line by line copy of the Perspectives section with the Roles column removed and the names changed. Let’s see how it looks.

States section added to widget

It looks good, and it has been pretty easy so far. The client-side functions should also be quite similar, although removing and altering States in the Table section of the JSON object will be a little more involved, as a State object appears in every Table in every Perspective. We’ll have to throw in a couple of nested loops to deal with every one of those when things change. Other than that, though, everything else should be a wholesale copy of the same code used in the Perspective section. Here are the new client-side functions:

$scope.editState = function(i) {
	var shared = {};
	if (i != 'new') {
		shared.label = c.data.config.state[i].label;
		shared.name = c.data.config.state[i].name;
	}
	spModal.open({
		title: 'State Editor',
		widget: 'e4bdae0d2f3b60104425fcecf699b649',
		shared: shared
	}).then(function() {
		if (i == 'new') {
			for (var x1 in c.data.config.perspective) {
				var p1 = c.data.config.perspective[x1].name;
				for (var y1 in c.data.config.table[p1]) {
					var table1 = c.data.config.table[p1][y1];
					table1[shared.name] = {btnarray: [], refmap: {}};
				}
			}
			i = c.data.config.state.length;
			c.data.config.state.push({});
		} else {
			if (shared.name != c.data.config.state[i].name) {
				for (var x2 in c.data.config.perspective) {
					var p2 = c.data.config.perspective[x2].name;
					for (var y2 in c.data.config.table[p2]) {
						var table2 = c.data.config.table[p2][y2];
						table2[shared.name] = table2[c.data.config.state[i].name];
						table2[c.data.config.state[i].name] = null;
					}
				}
			}
		}
		c.data.config.state[i].name = shared.name;
		c.data.config.state[i].label = shared.label;
	});
};

$scope.deleteState = function(i) {
	var confirmMsg = '<b>Delete State</b>';
	confirmMsg += '<br/>Deleting the ';
	confirmMsg += c.data.config.perspective[i].label;
	confirmMsg += ' State will also delete all information for that State in every Table in every Perspective.';
	confirmMsg += '<br/>Are you sure you want to delete this State?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			for (var x3 in c.data.config.perspective) {
				var p3 = c.data.config.perspective[x3].name;
				for (var y3 in c.data.config.table[p3]) {
					var table3 = c.data.config.table[p3][y3];
					table3[c.data.config.state[i].name] = null;
				}
			}
			c.data.config.state.splice(i, 1);
		}
	});
};

Other than the loops through the Perspectives and Tables and the missing Roles, it’s pretty much the exact same code that we had for the previous section. Now all we need to do is clone the Perspective Editor to create the State Editor, and this section should be completed as well. Dropping the Roles and changing the names leaves the HTML for the State Editor looking like this:

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="persp"
      snh-label="Name"
      snh-required="true"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

There is virtually no change at all to the client-side code, and there isn’t any server-side code, so we’re done. That was easy! Here is what it looks like in action:

The new State Editor modal dialog

So now we have completed two of our three sections, which makes it sound like we are two-thirds of the way there, but the next section is quite a bit more complicated than the first two. Tackling that guy sounds like a good place to start next time out.

Content Selector Configuration Editor, Part II

“If you concentrate on small, manageable steps you can cross unimaginable distances.”
Shaun Hick

Before I was sidetracked by my self-inflicted issues with my Configurable Data Table Widget Content Selector, I was just about to dive into the red meat of my new Content Selector Configuration Editor. Now that those issues have been resolved, we can get back to the fun stuff. As those of you who have been following along at home will recall, there are three distinct sections of the JSON object used to configure the content selector: 1) Perspective, 2) State, and 3) Table. Both the Perspective and State sections are relatively simple arrays of objects, but the Table section is much more complex, having properties for every State of every Table in every Perspective. Since I like to start out with the simple things first, my plan is to build out the Perspective section first, work out all of the kinks, and then pretty much clone that working model to create the State section. Once we get through all of that, then we can deal with the more complicated Table section.

As usual, we will start out with the visual components first and try to get things to look halfway decent before we crawl under the hood and wire everything together. Perspectives have only three properties, a Label, a Name, and an optional list of Roles to limit access to the Perspective. We should be able to lay all of this out in a relatively simple table, with one row for each Perspective. Here is what I came up with:

<div>
  <h4 class="text-primary">${Perspectives}</h4>
</div>
<div>
  <table class="table table-hover table-condensed">
    <thead>
      <tr>
        <th style="text-align: center;">Label</th>
        <th style="text-align: center;">Name</th>
        <th style="text-align: center;">Roles</th>
        <th style="text-align: center;">Edit</th>
        <th style="text-align: center;">Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in c.data.config.perspective">
        <td data-th="Name">{{item.label}}</td>
        <td data-th="Label">{{item.name}}</td>
        <td data-th="Roles">{{item.roles}}</td>
        <td data-th="Edit" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editPerspective($index)" alt="Click here to edit the details of this Perspective" title="Click here to edit the details of this Perspective" style="cursor: pointer;"/></td>
        <td data-th="Delete" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deletePerspective($index)" alt="Click here to permanently delete this Perspective" title="Click here to permanently delete this Perspective" style="cursor: pointer;"/></td>
      </tr>
    </tbody>
  </table>
</div>
<div style="width: 100%; text-align: right;">
  <button ng-click="editPerspective('new')" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to add a new Perspective">Add a new Perspective</button>
</div>

In addition to the columns for the three properties, I also added a column for a couple of action icons, one to edit the Perspective and one to delete the Perspective. I thought about putting input elements directly in the table for editing the values, but I decided that I would prefer to keep everything read-only unless you specifically asked to edit one of the rows. If you do want to edit one of the rows, then I plan on popping up a simple modal dialog where you can make your changes (we’ll get to that a little later).

I also added a button down at the bottom that you can use to add a new Perspective, which should pop up the same modal dialog without any existing values. Here’s what the layout looks like so far:

Perspective section of the configuration object editor

That’s not too bad. I think that it looks good enough for now. Our action icons reference nonexistent client-side functions right now, though, so next we ought to build those out. The Delete process looks like it might be the simplest of the two, so let’s say we start there. For starters, it’s always a good practice to pop up a confirm dialog before actually deleting anything, and I always like to use the spModal confirm option for that as opposed to a simple Javascript confirm. Since deleting the Perspective will also wipe out any Table information defined for that Perspective, we will want to warn them of that as well. Here is what I came up with:

$scope.deletePerspective = function(i) {
	var confirmMsg = '<b>Delete Perspective</b>';
	confirmMsg += '<br/>Deleting the ';
	confirmMsg += c.data.config.perspective[i].label;
	confirmMsg += ' Perspective will also delete all information for every State of every Table in the Perspective.';
	confirmMsg += '<br/>Are you sure you want to delete this Perspective?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			c.data.config.table[c.data.config.perspective[i].name] = null;
			c.data.config.perspective.splice(i, 1);
		}
	});
};

If the operator confirms the delete action, then we first null out all of the Table information for that Perspective, and then we slice out the Perspective from the list. We have to do things in that order. If we removed the Perspective first, then we would lose access to the name, which is needed to null out the Table data. Here is what the confirm dialog looks like on the page:

Perspective Delete Confirmation pop-up

That takes care of the easy one. Now on to the Edit action. For this one, we will use spModal as well, but instead of the confirm method we will be using the open method to launch a small Perspective Editor widget. The open method has an argument called shared that we can use to pass data to and from the widget. Here is the code to launch the widget and collect the updated data when it closes:

$scope.editPerspective = function(i) {
	var shared = {roles:{}};
	if (i != 'new') {
		shared.label = c.data.config.perspective[i].label;
		shared.name = c.data.config.perspective[i].name;
		shared.roles.value = c.data.config.perspective[i].roles;
		shared.roles.displayValue = c.data.config.perspective[i].roles;
	}
	spModal.open({
		title: 'Perspective Editor',
		widget: 'b83b9f342f3320104425fcecf699b6c3',
		shared: shared
	}).then(function() {
		if (i == 'new') {
			c.data.config.table[shared.name] = [];
			i = c.data.config.perspective.length;
			c.data.config.perspective.push({});
		} else {
			if (shared.name != c.data.config.perspective[i].name) {
				c.data.config.table[shared.name] = c.data.config.table[c.data.config.perspective[i].name];
				c.data.config.table[c.data.config.perspective[i].name] = null;
			}
		}
		c.data.config.perspective[i].name = shared.name;
		c.data.config.perspective[i].label = shared.label;
		c.data.config.perspective[i].roles = shared.roles.value;
	});
};

Since this function is intended to be used for both new and existing Perspectives, we have to check to see which one it is in a couple of places. Before we open the widget dialog, we will need to build a new object if this is a new addition, and after the dialog closes, we will have to establish a Table array for the new Perspective as well as establish an index value and an empty object at that index in the Perspective array. Also, if this is an existing Perspective and they have changed the name of the Perspective, then we need to move all of the associated Table information from the old name to the new name and get rid of everything under the old name. Other than that, the new and existing edit processes are pretty much the same.

This takes care of the client side function to launch the widget, but we still need to build the widget. That might get a little involved, and we have already covered quite a bit, so this may be a good place to stop for now. We’ll tackle that Perspective Editor widget first thing next time out, which should wrap up the Perspective section. Maybe we will even have time to clone it all and finish up the State section as well.

Configurable Data Table Widget Content Selector, Corrected

“No great thing is created suddenly.”
Epictetus

While playing around with my new Content Selector Configuration Editor, I ran into a few errors in my Configurable Data Table Widget Content Selector when working in the Portal Page Designer. The problem that I ran into was that errors in widget prevented the widget from appearing in the container, which then prevented you from accessing the widget controls that let you edit the widget options or delete the widget. I had run into something similar before with my Dynamic Service Portal Breadcrumbs, so I pretty much knew what was going on — I just needed to hunt down the specific error. In this particular case, it turned out to me more than one error, and fixing the first one still did not solve the problem completely.

The problem occurs when first dragging the widget onto a page. Before you have an opportunity to edit the widget options and specify a configuration script,the widget tries to run without a configuration script and then it crashes because it has no configuration script. Here is the offending code:

if (!c.data.table) {
	if (c.data.config.defaults.table) {
		refreshPage(c.data.config.defaults.table, c.data.config.defaults.perspective, c.data.config.defaults.state);
	} else {
		window.location.search = '';
	}
}

The problem is that second line that wants to grab the default table value from the defaults object in the configuration. Since we haven’t had a chance to specify a configuration script just yet, there is no defaults object in the configuration, and attempting to access the table property of that nonexistent object will earn you a NullPointerException. That’s not good! Before checking for the table property, we need to first check to see if the defaults object exists. This modification should do the trick:

if (!c.data.table) {
	if (c.data.config.defaults && c.data.config.defaults.table) {
		refreshPage(c.data.config.defaults.table, c.data.config.defaults.perspective, c.data.config.defaults.state);
	} else {
		$location.search('');
	}
}

That keeps the widget from crashing, but there is still no content displayed on the screen in the Page Designer, so I decided to add a little something to the top of the HTML that would only show if there was no configuration script specified. That code looks like this:

<div ng-hide="options && options.configuration_script">
  <div class="alert alert-danger">
    ${You must specify a configuration script using the widget option editor}
  </div>
</div>

Now when you drag a brand new copy of the widget onto the canvas in the Page Designer, you get this:

Content Selector widget with no configuration script specified

That takes care of an empty configuration script name, but what if you enter a name for script that isn’t a valid Script Include? Well, that crashes the widget code as well, so we will need to fix that, too. This time, the issue is on the server side, where we were assuming that we would always get an instance of whatever script was specified. As you can see from the following code snippet, we don’t even bother to check to see if something was returned from the Instantiator:

var configurator = instantiator.getInstance(options.configuration_script);
data.config = configurator.getConfig($sp);
data.config.authorizedPerspective = getAuthorizedPerspectives();
establsihDefaults();

That’s another easy fix, though. We just need to check to make sure that it is there before we attempt to use it.

var configurator = instantiator.getInstance(options.configuration_script);
if (configurator != null) {
	data.config = configurator.getConfig($sp);
	data.config.authorizedPerspective = getAuthorizedPerspectives();
	establishDefaults();
}

Here is another instance where it would be good to let the developer know that something is amiss, so I added yet anothe DIV to the top of the HTML:

<div ng-show="options && options.configuration_script && !data.config.defaults">
  <div class="alert alert-danger">
    {{options.configuration_script}} ${is not a valid Script Include}
  </div>
</div>

Here’s how that looks in action:

Error message when a invalid Script Include is specified

That should resolve all of the errors that I have discovered so far. Here is the corrected Update Set, which should replace all of the broken parts in the last one and get things working again.

Content Selector Configuration Editor

“Nothing in the world can take the place of Persistence. Talent will not; nothing is more common than unsuccessful men with talent. Genius will not; unrewarded genius is almost a proverb. Education will not; the world is full of educated derelicts. Persistence and Determination alone are omnipotent.”
Calvin Coolidge

Some time ago, I built a little Service Portal widget designed to allow a User to select various sets of records to be display in the Data Table widget on a portal page. I called this widget the Configurable Data Table Widget Content Selector because I designed it to be driven by an external JSON configuration object that could be specified as a widget option. Of course, I ended up just hard-coding the first one during my development and had to go back in later and fix it so that you could actually do that, but now that all works — you just have to build a new JSON configuration object if you want to use the widget on a different page for another purpose. That JSON object is a little complicated, though, so it occurred to me that it would be even better if there was some kind of input screen with some validation that would help you put one of those together. I did that not too long ago for the sn-record-picker, which seems to have worked out fairly well, so I was imagining something very similar, but maybe a little more complex.

There are three main sections to the Content Selector: 1) Perspectives, 2) States, and 3) Tables. My configuration wizard, then, would need a simple section where you could set up your Perspectives, another simple section where you could set up your States, and then a more complicated section where you could set up the Tables for every State of every Perspective. That third section sounds a little overwhelming at first glance, but like most complicated issues, if we break it down into its component parts and focus on one thing at time, we should be able to work through it.

The sn-record-picker Helper was just another portal widget, so that seemed like a decent approach to this endeavor as well. I called it the Content Selector Configurator, and placed it alone on a page of the same name for testing. To begin the process, we need to select an existing applicable Script Include, or enter a name if you want to create a brand new one. We can use an sn-record-picker for selecting an existing one, or to make things even easier, an snh-form-field of type reference, which is just a wrapper around the sn-record-picker that includes all of the labels and validation stuff. To limit the selection to just those Script Includes that are relevant to this process, we can filter on the field that contains the actual script looking for the code that extends the base class. The full snh-form-field element looks like this:

<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();"
  snh-required="true"
  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'"/>

… and renders out like this:

Script Include picker

When the selection is made, the snh-change attribute will invoke the scriptSelected() client-side function, so we will need to code that out as well to handle the choice that was made. All of the work necessary to handle the selection will actually occur on the server side, so the client side function just has to kick things over there.

$scope.scriptSelected = function() {
	c.server.update();
};

Over on the server side, things are a little more complicated. We need to use the name of the script to get an instance of the script, and for that, we can use our old friend, the Instantiator. Once we have an instance of the script, we can call the getConfig() function to get a copy of the current configuration object. but before we do that, we have to make sure that we have an input object and we don’t already have a config object. All together, the code looks like this:

if (input) {
	if (!data.config && input.script && input.script.value) {
		data.scriptInclude = input.script.value;
		if (data.scriptInclude.startsWith('global.')) {
			data.scriptInclude = data.scriptInclude.split('.')[1];
		}
		var instantiator = new Instantiator();
		instantiator.setRoot(this);
		var configScript = instantiator.getInstance(data.scriptInclude);
		data.config = configScript.getConfig($sp);
	}
}

Now that we have all of that out of the way, we can work on the actual wizard itself. I wrapped all of the HTML related to selecting a config script in one DIV and then made another, currently empty DIV for the wizard. I used complementary ng-show attributes to hide the wizard until the script was selected, and then hide the selection components once the choice was made. The whole thing now looks like this:

<snh-panel title="'${Content Selector Configuration Editor}'" class="panel-primary">
  <form id="form1" name="form1" ng-submit="save();" novalidate>
    <div class="row" ng-show="!c.data.script.value">
      <div class="col-sm-12">
        <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();"
          snh-required="true"
          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'"/>
      </div>
    </div>
    <div class="row" ng-show="c.data.script.value">
      (the wizard lives here)
    </div>
  </form>
</snh-panel>

I still have to add in the ability to create a new script from scratch, but I think I will deal with that later as I am anxious to jump into the wizard itself. I’ll circle back and toss that in before we wrap things up, but right now I just want to get on to fun stuff. That’s an entirely different process, though, so this seems like a good place to stop for now. We’ll jump straight into the wizard next time out.

Fun with Webhooks, Improved

“I think it’s very important to have a feedback loop, where you’re constantly thinking about what you’ve done and how you could be doing it better.”
Elon Musk

So far, I have had relatively good luck playing around with my Simple Webhooks app, and have been able to post content to other systems such as Slack and MS Teams in addition to the test cases that I sent over to webhook.site. One thing that I did notice, though, was that my portal page for editing the details of a Webhook was missing a couple of items found on the corresponding form in the main UI. On the form for a Webhook in the main UI, I built a UI Action that you can use to send a test POST to your URL, and the form also includes a Delete button that you can use to get rid of your Webhook when you no longer need it or want it. The current version of the portal page has neither of those features, so I decided that it was time to add those in.

The first order of business, then, was to add the two buttons to the HTML, right after the existing Save button:

&nbsp;
<button ng-show="c.data.sysId" ng-click="testURL()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to send a test POST to this URL">Test URL</button>
&nbsp;
<button ng-show="c.data.sysId" ng-click="deleteThis()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to permanently delete this webhook">Delete</button>

I didn’t want them showing up on new records, since there is no point in deleting a record that you haven’t created yet, so I added an ng-show attribute based on the presence of an existing sys_id. Other than that, it’s just a basic copy and paste of the other button code with some minor modifications. Here’s how it looks rendered out:

New buttons added to the form HTML

The new buttons reference new client-side functions, so next we will need to add those to the existing client-side script. Here are the two that I came up with:

$scope.testURL = function() {
	spModal.confirm('Send a test POST to this URL?').then(function(confirmed) {
		if (confirmed) {
			c.data.action = 'test';
			c.server.update();
		}
	});
};

$scope.deleteThis = function() {
	if (c.data.sysId) {
		spModal.confirm('Permanetly delete this Webhook?<br/>(This cannot be undone.)').then(function(confirmed) {
			if (confirmed) {
				c.data.action = 'delete';
				c.server.update().then(function(response) {
					goBack();
				});
			}
		});
	} else {
		goBack();
	}
};

I ended up putting a Confirm pop-up on both of them, even though technically the URL test is not destructive. I just thought that it might be nice to confirm that you really want send something over to another system before you actually did it. I also added the c.data.action variable so that once we were over on the server side, that code would know what to do. In our previous version, the only call to the server side was that Save button, so there was no question what needed to be done. But now that we have multiple possible actions, everyone — including Save — will need to register their intentions by setting this variable to some known value (save, test, or delete) before invoking c.server.update(). All of the actual work to perform the save, test, and delete actions is done on the server side, so let’s pop over there next.

To begin, I pulled out all of the existing Save logic and put it into a function of its own. Then I added the following conditional step, assuming that I would have similar functions for the other two actions:

	if (input) {
		if (input.action == 'save') {
			save(input);
		} if (input.action == 'test') {
			test(input);
		} if (input.action == 'delete') {
			deleteThis(input);
		}
	} else {
		. . . 

The Delete function turned out to be pretty basic stuff:

function deleteThis(input) {
	whrGR.get(input.sysId);
	whrGR.deleteRecord();
	gs.addInfoMessage('Your Webhook data has been deleted.');
}

Most of the code for the Test URL button I just stole from the existing UI Action built for the same purpose. Much of that is buried in the Script Include anyway, so that turned out to be fairly simple as well:

function test(input) {
	whrGR.get(input.sysId);
	var wru = new WebhookRegistryUtils();
	var result = wru.testURL(whrGR);
	if (result.status == '200') {
		gs.addInfoMessage('URL "' + input.url + '" was tested successfully.');
	} else {
		gs.addErrorMessage('URL test failed: ' + JSON.stringify(result, null, '<br/> '));
	}
}

That’s about all there was to that. Technically, you cannot really call this an enhancement since it is functionality that should have been in there from the start. Let’s just call it a much needed improvement. Here’s the new Update Set.

Automatically Link Referenced Tasks, Improved

“The secret of change is to focus all of your energy not on fighting the old, but on building the new”
Socrates

After running my LinkReferencedTasks Business Rule for a while, it has become apparent that there was a flaw in my approach. Whenever someone separates different task numbers with a special character, neither one gets picked up in the process. For example, if you enter something like REQ0010005/RITM0010009, the process that converts all special characters to an empty string ends up converting that to REQ0010005RITM0010009, which starts with REQ, but is not a valid Task number. A little Corrective Maintenance should solve that problem. Let’s convert this:

text = text.replace(/\n/g, ' ');
text = text.replace(/[^A-Z0-9 ]/g, '');

… to this:

text = text.replace(/[^A-Z0-9 ]/g, ' ');

Originally, I had converted all line feeds to spaces and then all characters that were not letters, numbers, or spaces to an empty string. Once I decided to change all characters that were not letters, numbers, or spaces to a space, I no longer needed the line above, as line feeds fall into that same category. That solved that problem.

However, while I was in there, I decided to do a little Perfective Maintenance as well. By changing all of the special characters to single spaces, I thought that I might end up with several spaces in a row, which would result in one or more empty strings in my array of words. To screen those out, I thought about discarding words with a length of zero, but then it occurred to me that short words of any kind will not be task numbers, so I settled on an arbitrary minimum length of 6 instead. Now the code that unduplicates the list of words looks like this:

var unduplicated = {};
for (var i=0; i<words.length; i++) {
	var thisWord = words[i];
	if (thisWord.length > 5) {
		unduplicated[thisWord] = thisWord;
	}
}

This should reduce the clutter quite a bit and minimize the number of words that need to be checked to see if they start with one of the specified prefixes.

And finally, I added an Enhancement, which I had actually thought about earlier, but didn’t implement. This enhancement allows you to specify the relationship type instead of just accepting the hard-coded Investigates relationship that I had put in the original. For backward compatibility, I did not want to make that mandatory, so I kept that as a default for those implementations that did not want to specify their own. The new version of the Script Include now looks like this:

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

	linkReferencedTasks: function(taskGR, prefix, relationship) {
		if (!relationship) {
			relationship = 'd80dc65b0a25810200fe91a7c64e9cac';
		}
		var text = taskGR.short_description + ' ' + taskGR.description;
		text = text.toUpperCase();
		text = text.replace(/[^A-Z0-9 ]/g, ' ');
		var words = text.split(' ');
		var unduplicated = {};
		for (var i=0; i<words.length; i++) {
			var thisWord = words[i];
			if (thisWord.length > 5) {
				unduplicated[thisWord] = thisWord;
			}
		}
		for (var word in unduplicated) {
			for (var x in prefix) {
				if (word.startsWith(prefix[x])) {
					this._findTask(word, taskGR.getUniqueValue(), relationship);
				}
			}
		}
	},

	_findTask: function(number, child) {
		var taskGR = new GlideRecord('task');
		if (taskGR.get('number', number)) {
			this._documentRelationship(taskGR.getUniqueValue(), child, relationship);
		}
	},

	_documentRelationship: function(parent, child, relationship) {
		var relGR = new GlideRecord('task_rel_task');
		relGR.addQuery('parent', parent);
		relGR.addQuery('child', child);
		relGR.query();
		if (!relGR.next()) {
			relGR.initialize();
			relGR.parent = parent;
			relGR.child = child;
			relGR.type = relationship;
			relGR.insert();
		}
	},

    type: 'TaskReferenceUtils'
};

So, a little fix here, a little improvement there, and a brand new feature over there, and suddenly we have a new version better than the last. Stuff’s getting better. Stuff’s getting better every day. Here’s the improved Update Set.

Automatically Link Referenced Tasks

“It is necessity and not pleasure that compels us.”
Dante Alighieri

Occasionally, we will get an Incident Ticket that references another task present in the system. This is usually a status request on a Service Catalog Request or a problem with a recent Change. It would be nice to be able to just click on those recognizable task numbers, but both the Short Description and the Description are plain text fields where that is not an option. There is, however, a handy out-of-the-box UI Formatter that you can include on your Incident Form that provides the capability to link other tasks to your Incident. The name of the Formatter is Task Relations, and I like to drag it onto the Incident Form in the Forms Designer right underneath the Formatter for Contextual Search Results. Once you have that Formatter present on your Incident Form, you can click on the green plus sign and link other tasks of various types to your Incident.

That’s a really cool feature that allows you click on the related tasks to open them up, but being the lazy developer that I am, I would prefer not to have to go through all of the manual work to set up all of the links to the things mentioned in the text of the ticket. In my ideal world, the system would be smart enough to read the text, recognize a task number, and then build the link for me so that I don’t have to do all of that work by hand, and also to make sure that I did not miss anything. How hard could that be?

My thought was that I could create a Business Rule linked to the Incident table that would examine the two primary text fields (Short Description and Description), look for anything that appeared to be a task number, search the Task table to see if it really was a task number, and if so, build the link for me. It seemed like a relatively simple thing to do, so I went to work.

It felt as if there might be a lot of code involved, so instead of putting all of that in the Business Rule itself, I decided to build a Script Include that I could call from the Business Rule. I thought that if I could make the function generic enough, I might be able to reuse it for other Business Rules linked to different Task tables. Plus, putting all of the code in the Script Include keeps the Business Rule a lot cleaner. Here are the things that I thought that I needed to do in order to get this all to work:

  • Combine the two text fields on the Incident into a single string variable for examination,
  • Convert the text to upper case,
  • Convert all line feeds to spaces,
  • Remove all characters that are not letters, numbers, or spaces,
  • Split the string by spaces creating an array of words,
  • Unduplicate the array of words so that we only looked at each unique word once,
  • Examine each word to see if it started with a known task number prefix,
  • Read the Task table for every word that started with a known task number prefix, and
  • Build a relationship record for every Task record that was found on the Task table.

All we need to do now is turn that into code.

Combine the two text fields on the Incident into a single string variable for examination:

var text = taskGR.short_description + ' ' + taskGR.description;

Convert the text to upper case:

text = text.toUpperCase();

Convert all line feeds to spaces:

text = text.replace(/\n/g, ' ');

Remove all characters that are not letters, numbers, or spaces:

text = text.replace(/[^A-Z0-9 ]/g, '');

Split the string by spaces creating an array of words:

var words = text.split(' ');

Unduplicate the array of words so that we only looked at each unique word once:

var unduplicated = {};
for (var i=0; i<words.length; i++) {
	var thisWord = words[i];
	unduplicated[thisWord] = thisWord;
}

Examine each word to see if it started with a known task number prefix:

for (var word in unduplicated) {
	for (var x in prefix) {
		if (word.startsWith(prefix[x])) {
			this._findTask(word, taskGR.getUniqueValue());
		}
	}
}

Read the Task table for every word that started with a known task number prefix:

_findTask: function(number, child) {
	var taskGR = new GlideRecord('task');
	if (taskGR.get('number', number)) {
		this._documentRelationship(taskGR.getUniqueValue(), child);
	}
},

Build a relationship record for every Task record that was found on the Task table:

_documentRelationship: function(parent, child) {
	var relGR = new GlideRecord('task_rel_task');
	relGR.addQuery('parent', parent);
	relGR.addQuery('child', child);
	relGR.query();
	if (!relGR.next()) {
		relGR.initialize();
		relGR.parent = parent;
		relGR.child = child;
		relGR.type = 'd80dc65b0a25810200fe91a7c64e9cac';
		relGR.insert();
	}
},

Before I create a relationship record, I want to make sure that there isn’t already a relationship record out there, so I do a quick query just to check before I commit to inserting a new one. The two records don’t need to be linked more than once. I also hard-coded the relationship type, which works for my current purpose, but if I ever want to expand this out to other use cases, I may eventually want to pass that in as an argument as I did with the task number prefixes. Here is the whole thing, all put together:

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

	linkReferencedTasks: function(taskGR, prefix) {
		var text = taskGR.short_description + ' ' + taskGR.description;
		text = text.toUpperCase();
		text = text.replace(/\n/g, ' ');
		text = text.replace(/[^A-Z0-9 ]/g, '');
		var words = text.split(' ');
		var unduplicated = {};
		for (var i=0; i<words.length; i++) {
			var thisWord = words[i];
			unduplicated[thisWord] = thisWord;
		}
		for (var word in unduplicated) {
			for (var x in prefix) {
				if (word.startsWith(prefix[x])) {
					this._findTask(word, taskGR.getUniqueValue());
				}
			}
		}
	},

	_findTask: function(number, child) {
		var taskGR = new GlideRecord('task');
		if (taskGR.get('number', number)) {
			this._documentRelationship(taskGR.getUniqueValue(), child);
		}
	},

	_documentRelationship: function(parent, child) {
		var relGR = new GlideRecord('task_rel_task');
		relGR.addQuery('parent', parent);
		relGR.addQuery('child', child);
		relGR.query();
		if (!relGR.next()) {
			relGR.initialize();
			relGR.parent = parent;
			relGR.child = child;
			relGR.type = 'd80dc65b0a25810200fe91a7c64e9cac';
			relGR.insert();
		}
	},

    type: 'TaskReferenceUtils'
};

Now that we our Script Include, we need to build the Business Rule that calls it. For my purpose, I added a Business Rule to the Incident table and called it LinkReferencedTasks. I checked the Advanced checkbox and made it active async on Insert whenever there was text in either of the two description fields. I could have also triggered it on Update as well, but in my experience, the Incident description is usually captured when the Incident is created an rarely updated after that.

LinkReferencedTasks Business Rule

Under the Advanced tab, I entered the following script:

(function executeRule(current, previous) {
	new TaskReferenceUtils().linkReferencedTasks(current, ['REQ','RITM','SCTASK','CHG']);
})(current, previous);

In addition to the current GlideRecord, you also need to pass in a string array of task table prefixes which will be used somewhat like a filter to only link tasks that start with those values. If you want a different mix of task types, you can just update that list. That’s it. We are done. Well, maybe we had better test it out first, but the building part is done anyway. Testing should be simple enough: we just need to find an existing task that we want to reference and then include that task number in the text of a new incident. Let’s do that now.

New test Incident with references to other tasks in the Short Description field

Now all we have to do is hit that Submit button and see if those tasks referenced in the text are automatically linked to the Incident. Seems as if a little drum-roll would be appropriate here …

Test results for the new Business Rule and Script Include

Well, nothing ever goes right the very first time! It looks like we managed to create a link to the RITM, but not to the Request. Fortunately, it turns out that this is a tester error and not a developer error. When I typed in the Request number, I missed a zero. The actual Request number is REQ0010005, not REQ001005. Of course, my explanation for that is that I did that on purpose to demonstrate that it will only link real requests, and if your request number is not a real request, then it won’t bother to attempt to create a link to it. That’s it — I did it on purpose — it was all part of the plan. You know that you have to test for failure as well as success — I just did it all in one test because I am so efficient. Sometimes I even amaze myself!

Anyway, it all seems to work, so for those of you who like to play along at home, here’s an Update Set with all of the parts.

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