Flow Designer Array Iterator

“It is by logic that we prove, but by intuition that we discover.”
Henri Poincaré

One of the reasons that I built my little Flow Designer Scratchpad was to keep track of an index while I looped through items in an Array. After doing a few of those, I decided it would be even nicer if I had some kind of iterator Action that would do the work of incrementing the index and returning the Array element at the current index. I already had the scratchpad to store all of the data necessary to support an iterator, so it seemed as if I could write some kind of Action that would take a Scratchpad ID, an Iterator ID, and an Array as input and use that information to set up the ability to iterate through the Array provided. As usual, I decided to put the bulk of the code in a Script Include to keep the actual code in the Action down to the absolute minimum. Here is the SNHStringArrayUtils that I came up with:

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

	createIterator: function(scratchpadId, interatorId, stringArray) {
		var response = {};

		var snhspu = new SNHScratchpadUtils();
		response = snhspu.setScratchpadProperty(scratchpadId, interatorId, interatorId);
		if (response.success) {
			if (Array.isArray(stringArray)) {
				var iterator = {};
				iterator.index = 0;
				iterator.array = stringArray;
				response = snhspu.setScratchpadProperty(scratchpadId, interatorId, JSON.stringify(iterator));
				if (response.success) {
					response.message = 'Array Interator ' + interatorId + ' successfully created';
				}
			} else {
				response.success = false;
				response.message = 'String Array parameter is missing or invalid';
			}
		}

		return response;
	},

	iteratorNext: function(scratchpadId, interatorId) {
		var response = {};

		var snhspu = new SNHScratchpadUtils();
		response = snhspu.getScratchpadProperty(scratchpadId, interatorId);
		if (response.success) {
			var iterator = {};
			try {
				iterator = JSON.parse(response.property_value);
			} catch (e) {
				response.success = false;
				response.message = 'Unable to parse JSON string containing iterator details';
			}
			if (response.success) {
				if (iterator.index >= 0 && iterator.index < iterator.array.length) {
					response.current_value = iterator.array[iterator.index];
					response.current_index = iterator.index;
					iterator.index++;
					response.has_next = (iterator.index < iterator.array.length);
					response.message = 'The current value at index ' + response.current_index + ' is ' + response.current_value;
					snhspu.setScratchpadProperty(scratchpadId, interatorId, JSON.stringify(iterator));
				} else {
					response.success = false;
					response.message = 'Current index value out of range';
				}
			}
		}

		return response;
	},

	type: 'SNHStringArrayUtils'
};

Basically, there are two methods, one for each of the two Flow Designer Actions that I intend to build. The first one is createIterator, which is used to initialize a new iterator, and the second is iteratorNext, which will support the Action that you will invoke inside of your loop to get the next item in the Array. Both utilize an existing scratchpad, so you will need to create that prior to invoking these Actions, and both require an Iterator ID, which is just a unique key to be used in storing the iterator data in the scratchpad. The createIterator action would be called once outside of the loop, and then the iteratorNext function would be called inside of the loop, usually right at the top to pull out the next value in the array.

The iterator itself is just a two-property object containing the array and the current value of the index. This is converted to a JSON string and stuffed into the scratchpad using the passed Iterator ID as the key. When creating the iterator, we set the index value to zero, and in the next Action, after using the index to get the current element, we increment it and update the scratchpad. Now that we have the basic code to support the two Actions, we need to go into the Flow Designer and create the Actions.

The Create Array Iterator Action seems like the logical place to start. That will will need three Inputs defined.

The Create Array Iterator Action Inputs

… and it will need two Outputs defined, a success indicator and an optional error message detailing any failure to perform its intended function.

The Create Array Iterator Action Outputs

In between the Inputs and Outputs will be a simple Script step, where we will produce the Outputs by passing the Inputs to our associated Script Include function.

var snhsau = new SNHStringArrayUtils();
var result = snhsau.createIterator(inputs.scratchpad_id, inputs.iterator_id, inputs.string_array);
for (var key in result) {
	outputs[key] = result[key];
}

That’s pretty much all there is to that. We can test it using the Test button up at the top of the Action Editor, but first we will need a Scratchpad. We can take care of that real quick by hitting the Test button on the Create Scratchpad Action and then grabbing the Scratchpad ID from the Outputs. With our Scratchpad ID in hand, we can now test our Create Array Iterator Action.

Testing the Create Array Iterator Action

So far so good. now we just need to do the same thing for the Array Iterator Next Action, and we’ll be all set.

The Array Iterator Next Action

This is pretty much a rinse and repeat kind of thing, with fewer Inputs, but more Outputs. When it comes time to test, we can use the Scratchpad ID and Iterator ID from our last test, and then run it through a few times to see the different results at different stages of the process. Rather that go through all of that here, I will just bundle everything up into an Update Set, and you can pull it down and play with it on your own.

Note: With the introduction of Flow Variables, this component is no longer necessary.

Dynamic Service Portal Breadcrumbs, Enhanced, Part II

“There are three kinds of men. The one that learns by reading. The few who learn by observation. The rest of them have to pee on the electric fence for themselves.”
Will Rogers

I meant to do this earlier, but I got a little sidetracked on a completely different issue. Now that that little adventure is behind us, we can circle back to my recent changes to the Dynamic Service Portal Breadcrumbs and test everything out to see if it actually works. For that, I need to build a simple Portal Page, drop the breadcrumbs widget up at the top of the page, and then add a new widget that can listen for the messages coming out of the breadcrumbs widget.

So, let’s start with the new widget. This is just for testing, so we don’t need to get too fancy. Just a couple of test buttons, one to call for the return path and the other a back button that will use the return path. Here is some HTML that should work:

<div class="panel">
  <div class="row" style="text-align: center; padding: 25px;">
    <button class="btn btn-default" ng-click="testFunction();">Test Button</button>
  </div>
  <div class="row" style="text-align: center; padding: 25px;">
    <button class="btn btn-primary" ng-click="goBack();">Back Button</button>
  </div>
</div>

Each button has a function so will need to code those out in the Client Script to handle the clicks:

$scope.testFunction = function() {
	$rootScope.$broadcast('snhShareReturnPath');
};

$scope.goBack = function() {
	if (!c.data.returnPath) {
		$rootScope.$broadcast('snhShareReturnPath');
	}
	while (!c.data.returnPath) {
		pointlessCounter++;
	}
	window.location.href = c.data.returnPath;
};

The first one just broadcasts the message that we missed the return path, so please send it over again. The second one, which is an actual back button, looks to see if we have already obtained the return path, and if not, makes the same call to request it, and the drops into a loop until it shows up. I threw a counter in there just for something to do inside of the loop, but if I display that out, I can also see how long it takes to hear back from the request to rebroadcast the return path. Speaking of receiving the return path, we will need to add a little code to listen for that as well:

$rootScope.$on('snhBreadcrumbs', function(evt, info) {
	c.data.returnPath = info.returnPath;
	alert(pointlessCounter + '; ' + c.data.returnPath);
});

Wrapping all of that together with a little initialization code give us a complete Client Script for our new widget:

function TestWidget($scope, $rootScope) {
	var c = this;
	var pointlessCounter = 0;

	$scope.testFunction = function() {
		$rootScope.$broadcast('snhShareReturnPath');
	};

	$scope.goBack = function() {
		if (!c.data.returnPath) {
			$rootScope.$broadcast('snhShareReturnPath');
		}
		while (!c.data.returnPath) {
			pointlessCounter++;
		}
		window.location.href = c.data.returnPath;
	};

	$rootScope.$on('snhBreadcrumbs', function(evt, info) {
		c.data.returnPath = info.returnPath;
		alert(pointlessCounter + '; ' + c.data.returnPath);
	});
}

There is no server side script needed, so that completes our tester. Now we just have to throw it on a page and see what happens.

Test Page with the breadcrumbs and test widgets

Well, there is our test page containing the two widgets. There is no alert, though, which means that no one was listening when the breadcrumbs widget announced the return path, or something is broken. Let’s try the Test Button to see if we can get the breadcrumbs widget to announce it again.

Alert indicating the test widget has received the broadcast message

OK, that worked. Now, let’s try the back button.

The back button takes you back the previous screen in the breadcrumbs list

.. and that takes us back to the Incident that we were just looking at before we brought up the test page. Nice!

So far, so good. I tried a few more things, like clicking on the back button without first clicking on the test button and clicking on the test button multiple times. Everything seems to work the way in which I had envisioned it. I like it. As far as I can tell, it is good enough for me to put out another Update Set. Although I did get a little sidetracked on this one, it was a quick diversion and I did manage to circle back around and finish it up.

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

Flow Designer Scratchpad

“Necessity is the mother of invention.”
English-language proverb

I really should be working on testing out the latest enhancements to my Dynamic Service Portal Breadcrumbs right now, but I ran into this other issue recently, and I really want to see if I can make this work. I hate it when people start something and then move on to other things without ever finishing up what they started, so I definitely want to circle back and wrap that one up; however, today is not that day.

Today I want to talk about the Flow Designer. I have been striving to convert any of my old legacy Workflows over to the newer Flow Designer tool whenever the opportunity arises. The other day I was doing just that with a Workflow that made extensive use of the Workflow Scratchpad feature. When I went to look for the equivalent feature in the Flow Designer, I couldn’t find anything. I thought maybe that it wasn’t needed for some reason, so I tried several workarounds, but nothing worked. Nothing that I tried would preserve and/or modify data between or across Actions or Subflows. After quite a number of failed attempts to find something that would do the job, I eventually came to realize that if I wanted some kind of Scratchpad capability in the Flow Designer, I was going to have to build it myself.

My first thought was that all that I would need would be simple setProperty and getProperty functions, but then I realized that I would first need to establish the scratchpad, and once established, I would want to be able to get rid of it as well, so that turned into four relatively simple functions, which is still not too bad. When I say functions, what I really mean are Flow Designer Actions, but since I will be calling some function in a common Script Include built for this purpose, I still think of them as functions. Here is the function to create a scratchpad, which is just basically a record on a table that I created for this purpose:

createScratchpad: function() {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	spGR.initialize();
	spGR.setValue('u_scratchpad', '{}');
	if (spGR.insert()) {
		response.success = true;
		response.scratchpad_id = spGR.getUniqueValue();
		response.message = 'Scratchpad record successfully created';
	} else {
		response.success = false;
		response.message = 'Unable to create scratchpad record';
	}

	return response;
},

The scratchpad itself is just a JSON string stored in the only column added to the table, u_scratchpad. We initialize that to an empty object and save the record and that’s about all there is to that. To get rid of it, we will need to have the sys_id of the record, but there is not much code behind that process, either:

deleteScratchpad: function(spId) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		if (spGR.deleteRecord()) {
			response.success = true;
			response.message = 'Scratchpad record successfully deleted';
		} else {
			response.success = false;
			response.message = 'Unable to delete scratchpad record';
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

That takes care of building up and tearing down the scratchpad object. Now, to use it, we will need those setProperty and getProperty functions that we were talking about earlier. This one will let you set the value of a property on the scratchpad:

setScratchpadProperty: function(spId, propertyName, propertyValue) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		var jsonString = spGR.getValue('u_scratchpad');
		var jsonObject = {};
		try {
			jsonObject = JSON.parse(jsonString);
		} catch(e) {
			response.warning = 'Unable to parse JSON string: ' + e;
		}
		jsonObject[propertyName] = propertyValue;
		jsonString = JSON.stringify(jsonObject, null, '\t');
		spGR.setValue('u_scratchpad', jsonString);
		if (spGR.update()) {
			response.success = true;
			response.message = 'Scratchpad property "' + propertyName + '" set to "' + propertyValue + '"';
		} else {
			response.success = false;
			response.message = 'Unable to update scratchpad record';
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

… and this one lets you retrieve the value of a property on the scratchpad:

getScratchpadProperty: function(spId, propertyName) {
	var response = {};

	var spGR = new GlideRecord('u_snh_scratchpad');
	if (spGR.get(spId)) {
		var jsonString = spGR.getValue('u_scratchpad');
		try {
			var jsonObject = JSON.parse(jsonString);
			var propertyValue = jsonObject[propertyName];
			if (propertyValue > '') {
				response.success = true;
				response.property_value = propertyValue;
				response.message = 'Returning value "' + propertyValue + '" for scratchpad property "' + propertyName + '"';
			} else {
				response.success = false;
				response.message = 'Scratchpad property "' + propertyName + '" has no value';
			}
		} catch(e) {
			response.success = false;
			response.message = 'Unable to parse JSON string: ' + e;
		}
	} else {
		response.success = false;
		response.message = 'Scratchpad record not found';
	}

	return response;
},

That’s it for the core functions needed to make this work. Putting it all together, the entire Script Include looks like this:

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

	createScratchpad: function() {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		spGR.initialize();
		spGR.setValue('u_scratchpad', '{}');
		if (spGR.insert()) {
			response.success = true;
			response.scratchpad_id = spGR.getUniqueValue();
			response.message = 'Scratchpad record successfully created';
		} else {
			response.success = false;
			response.message = 'Unable to create scratchpad record';
		}

		return response;
	},

	setScratchpadProperty: function(spId, propertyName, propertyValue) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			var jsonString = spGR.getValue('u_scratchpad');
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch(e) {
				response.warning = 'Unable to parse JSON string: ' + e;
			}
			jsonObject[propertyName] = propertyValue;
			jsonString = JSON.stringify(jsonObject, null, '\t');
			spGR.setValue('u_scratchpad', jsonString);
			if (spGR.update()) {
				response.success = true;
				response.message = 'Scratchpad property "' + propertyName + '" set to "' + propertyValue + '"';
			} else {
				response.success = false;
				response.message = 'Unable to update scratchpad record';
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	getScratchpadProperty: function(spId, propertyName) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			var jsonString = spGR.getValue('u_scratchpad');
			try {
				var jsonObject = JSON.parse(jsonString);
				var propertyValue = jsonObject[propertyName];
				if (propertyValue > '') {
					response.success = true;
					response.property_value = propertyValue;
					response.message = 'Returning value "' + propertyValue + '" for scratchpad property "' + propertyName + '"';
				} else {
					response.success = false;
					response.message = 'Scratchpad property "' + propertyName + '" has no value';
				}
			} catch(e) {
				response.success = false;
				response.message = 'Unable to parse JSON string: ' + e;
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	deleteScratchpad: function(spId) {
		var response = {};

		var spGR = new GlideRecord('u_snh_scratchpad');
		if (spGR.get(spId)) {
			if (spGR.deleteRecord()) {
				response.success = true;
				response.message = 'Scratchpad record successfully deleted';
			} else {
				response.success = false;
				response.message = 'Unable to delete scratchpad record';
			}
		} else {
			response.success = false;
			response.message = 'Scratchpad record not found';
		}

		return response;
	},

	type: 'SNHScratchpadUtils'
};

Now that we have all of the functions, we need to turn those into Flow Designer Actions. Before we do that, though, let’s create a Category for them so that we can group them and they will be easy to find. We do that by adding a row to the Action Category table, sys_hub_category. With that out of the way, we can create our first Action, which will invoke the createScratchpad function in our Script Include.

Create Scratchpad Action

The entire Action is just a Script step that leverages our Script Include and passes the results on to the Action Outputs. The small script to make that happen is just a few short lines of code:

var snhspu = new SNHScratchpadUtils();
var result = snhspu.createScratchpad();
for (var key in result) {
	outputs[key] = result[key];
}

Now we just need to repeat that process 3 more times to create Flow Designer Actions from our three other Script Include functions and we’re all set. To test things out, there is a little Test button right at the top of the Flow Designer page, and for the Create Scratchpad Action, there isn’t even any input to set up, so you can just click that button and go. Once you test out the Create Scratchpad Action, you can snag the Scratchpad ID out of the Action Outputs and then use that as an input to test all of the others.

Well, that wasn’t so bad: one Script Include, four functions, one Action Category, and four Actions. I threw this together rather quickly, but here is the Update Set. If you run into any issues with that, or if you can think of any way to make it better, please let me know in the comments. Of if you know of a built-in function that eliminates the need for this, that would be even better!

Note: With the introduction of Flow Variables, this component is no longer necessary.

Dynamic Service Portal Breadcrumbs, Enhanced

“Build your own dreams, or someone else will hire you to build theirs.”
Farrah Gray

While I was playing around with my Configuration Item Icon Assignment widget, it occurred to me that it would be beneficial for my Dynamic Service Portal Breadcrumbs widget to somehow announce its presence to other widgets on the same page, and to provide them with the URL of previous page in case they wanted to set up some kind of Cancel or Done button that returned the user to the page from which they came. I even thought about putting such a button on the breadcrumbs widget itself, just under the row of breadcrumbs, but that seemed a little out of place, the more that I thought about it. So right now, I just want to let the other widgets know that they are sharing the page with the breadcrumbs widget and provide them with the return path.

The easiest way to do that is through some kind of root scope broadcast message, but that does present a little bit of a wrinkle, as you don’t really know which widgets have loaded first, and you could be broadcasting to no one if you are first to arrive on the scene. To solve that problem, you can listen for a different root scope message from other widgets that basically asks, “Please say that again now that I am ready to hear it.” From the listener’s perspective then, when you are ready to utilize the return path, if you haven’t yet received it, you can request it, and then go from there.

But first things first. Before we can announce the return path, we need to know what it is. Using the length of the breadcrumbs array as an index takes you past the end of the array, but subtracting 1 from that value gets you to the last entry, which happens to be the current page. We want the page before that, so we have to subtract 2 from the length. The danger there, of course, is that you might have an empty array or an array with a single element, and subtracting 2 would again put your index outside of the range of the array. So we have to start out with a default value, and then check the length of the array before we proceed any further. My code to do that looks like this:

c.returnPath = '?';
if (c.breadcrumbs.length > 1) {
	c.returnPath = c.breadcrumbs[c.breadcrumbs.length - 2].url;
}

That establishes the return path. Now we have to announce it. I created a function for that, since we will be doing this from multiple places (on load, and then again on request). Here is my function:

function shareReturnPath() {
	$rootScope.$broadcast('snhBreadcrumbs', {
		returnPath: c.returnPath
	});
}

Now that we have created the function, we need to call it as soon as we establish the return path, and then we need to set up a listener for later requests that will call it again on demand. For that, we just need a little more code wedged in between the two blocks above:

shareReturnPath();
$rootScope.$on('snhShareReturnPath', function() {
	shareReturnPath();
});

Well, that wasn’t so bad. Combined with what we had before, the entire Client Controller now looks like this:

function snhBreadcrumbs($scope, $rootScope, $location, spUtil) {
	var c = this;
	c.expanded = !spUtil.isMobile();
	c.expand = function() {
		c.expanded = true;
	};
	c.breadcrumbs = [];
	var thisTitle = c.data.page || document.title;
	var portalSuffix = ' - ' + $rootScope.portal.title.trim();
	var cutoff = thisTitle.indexOf(portalSuffix);
	if (cutoff != -1) {
		thisTitle = thisTitle.substring(0, cutoff);
	}
	var thisPage = {url: $location.url(), id: $location.search()['id'], label: thisTitle};
	
	if (thisPage.id && 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();
	c.returnPath = '?';
	if (c.breadcrumbs.length > 1) {
		c.returnPath = c.breadcrumbs[c.breadcrumbs.length - 2].url;
	}
	shareReturnPath();
	$rootScope.$on('snhShareReturnPath', function() {
		shareReturnPath();
	});

	function shareReturnPath() {
		$rootScope.$broadcast('snhBreadcrumbs', {
			returnPath: c.returnPath
		});
	}
}

Yes, we still have to test it, just to make sure that it all works in the way in which we intended, but it should work and it didn’t take all that much to provide this new functionality. In fact, it will probably be more work to test it than it was to create it. I’ll have to find or build another widget to share the page with this one, then create a page to put them both on, and then bring up the page and try things out. Now that I think about it, that seems like a decent amount of effort, so I think I will just save all of that until next time!