Collaboration Store, Part XLIX

“Don’t tell me the sky’s the limit when there are footprints on the moon.”
Paul Brandt

Last time, we got started on the global UI Script that will run on the upload.do page to take over the page and repurpose it for our needs. Our interest is to convert an Update Set XML file back into an actual Update Set so that we can apply the Update Set, installing a shared Scoped Application. The upload.do page will help set us on that path, but we need our script to implement just a few little modifications. We got as far as launching the GlideAjax process which will fetch the Update Set XML file details from the server side, and now we need to build the function that will process the results coming back and do something with them. The “answer” returned will be a JSON string, so we just need to turn that back into an object so that we can extract the values. We can do just that much and verify the results by popping an alert using one of the values that should be found in the resulting object, the name of the XML file.

function submitForm(answer) {
	var app = {};
	try {
		app = JSON.parse(answer);
	} catch (e) {
		alert('Error parsing JSON response from server: ' + e);
	}
	alert(app.fileName);
}

There is not much here, but we can push the old Install button on the version page, just to verify that all is well so far.

Verification of the server side code, Ajax call, and JSON parsing

Although that wasn’t much in the way of code, it did verify that the server side Script Include that we built a while back does seem to work, as well as the Ajax call that we built last time and the JSON parsing that we just added today. At this point, we have built a UI Action that sends us over to the upload.do page, taken over the page for our own purposes, hiding the original content and adding content of our own, called back to the server side for the XML file information, and demonstrated that the XML file information has indeed been transferred over to the client side. Now that we have it in hand, we have to use it to emulate a file on the local system and send that faux file back over to the server side as an element of a form post. This is where things get a little tricky.

While digging around trying to find a way to do this, I came across the DataTransfer object. This object contains a list of File objects, and you can add to the list using the add() method of the items property. These two lines of code create a new DataTransfer object and add a new file to the empty list using the data that we retrieved from the Ajax call.

var fileList = new DataTransfer();
fileList.items.add(new File([app.xml], app.fileName, {type: 'application/xml'}));

Now that we have our “file” in a file list, we can populate the files attribute of the input element using the files attribute of our DataTransfer object.

document.getElementById('attachFile').files = fileList.files;

Now we just have to submit the form and see what happens. Actually, I did that, and nothing happened. It seems that there are a couple of other form fields that also need to be valued. What seems weird to me is that, if you look at the source code for the page, those fields do start out with a value, but somewhere along the line those values were removed before the form was posted, so I had to add a couple more lines to put those values back.

document.getElementsByName('sysparm_referring_url')[0].value = 'sys_remote_update_set_list.do?sysparm_fixed_query=sys_class_name=sys_remote_update_set';
document.getElementsByName('sysparm_target')[0].value = 'sys_remote_update_set';

Now we can submit the form, which is just one more line of code.

document.forms[0].submit();

All together, our new submitForm function looks like this:

function submitForm(answer) {
	var app = {};
	try {
		app = JSON.parse(answer);
	} catch (e) {
		alert('Error parsing JSON response from server: ' + e);
	}
	var fileList = new DataTransfer();
	fileList.items.add(new File([app.xml], app.fileName, {type: 'application/xml'}));
	document.getElementById('attachFile').files = fileList.files;
	document.getElementsByName('sysparm_referring_url')[0].value = 'sys_remote_update_set_list.do?sysparm_fixed_query=sys_class_name=sys_remote_update_set';
	document.getElementsByName('sysparm_target')[0].value = 'sys_remote_update_set';
	document.forms[0].submit();
}

And that completes (for now) our new global UI Script. Here is the entire script, including all of the work that we did last time out.

if (window.location.pathname == '/upload.do' && window.location.search.startsWith('?attachment_id=')) {
	waitForPageLoad();
}

function waitForPageLoad() {
	if (document.getElementById('attachFile')) {
		installApplication();
	} else {
		setTimeout(waitForPageLoad, 100);
	}
}

function installApplication() {
	var originalContent = document.getElementsByClassName('section-content')[0];
	originalContent.style.visibility = 'hidden';
	var newContent = document.createElement('div');
	newContent.innerHTML = '<h4 style="padding: 30px;">&nbsp;<img src="/images/loading_anim4.gif" height="18" width="18">&nbsp;Uploading Update Set XML file ...</h4>';
	originalContent.parentNode.insertBefore(newContent, originalContent);
	var attachmentId = window.location.search.substring(15);
	var ga = new GlideAjax('x_11556_col_store.ApplicationInstaller');
	ga.addParam('sysparm_name', 'getXML');
	ga.addParam('attachment_id', attachmentId);
	ga.getXMLAnswer(submitForm);
}

function submitForm(answer) {
	var app = {};
	try {
		app = JSON.parse(answer);
	} catch (e) {
		alert('Error parsing JSON response from server: ' + e);
	}
	var fileList = new DataTransfer();
	fileList.items.add(new File([app.xml], app.fileName, {type: 'application/xml'}));
	document.getElementById('attachFile').files = fileList.files;
	document.getElementsByName('sysparm_referring_url')[0].value = 'sys_remote_update_set_list.do?sysparm_fixed_query=sys_class_name=sys_remote_update_set';
	document.getElementsByName('sysparm_target')[0].value = 'sys_remote_update_set';
	document.forms[0].submit();
}

At this point, all that we have accomplished is to load the Update Set. We still have not installed anything. The Update Set still has to be Previewed and then Committed before the version is actually installed. The ultimate goal will be for the operator to be able to just click on that Install button and have everything else takes care of itself, including marking the version record as Installed (and any other version records of the app as not installed). Whether or not we can do all of that without human intervention has yet to be determined, but we have at least accomplished that first step of turning the XML file back into an Update Set. Next time, we will see where we can go from here.

Collaboration Store, Part XLVIII

“Most times, the way isn’t clear, but you want to start anyway. It is in starting that other steps become clearer.”
Israelmore Ayivor

Last time, we created a process to retrieve the Update Set XML data from the server side and then built a UI Action to launch the installation process. At the time that we left off, I was vacillating back and forth between hacking up the original upload.do page and creating a customized copy of my own. Since that time, though, I have decided that I am much too lazy to try to build one of my own, so I am just going to attempt to hack up the one that already exists with as minimal amount of intervention as I can muster. The one way that I know how to do that is to create a global UI Script that modifies the page on the fly without actually altering the source of the page itself. We have already used this technique with our earlier incident email hack, so at least we know that this approach is one that will work.

Unfortunately, you cannot create global UI Scripts in a Scoped Application; the script has to be in the global scope, so this component will be yet another addition to our global components Update Set. I don’t really like having all of these parts outside of the application, but that’s just the way that these things go sometimes. These global scripts run on every single page load in the system, so to be a minimally intrusive as possible, the very first thing that you want to check is whether or not you are running on a page in which this code is needed. For our purposes, we only want this code to run on the upload.do page, and only if our attachment_id parameter is present in the URL.

if (window.location.pathname == '/upload.do' && window.location.search.startsWith('?attachment_id=')) {
	alert('So far, so good ...');
}

We can test this out by going into a version record and clicking on the new Install form button.

First test of the new global UI Script

OK, that works. In fact, that also proves out the code on the UI Action that we created last time. As the alert says, so far, so good. One thing that you will notice, however, is that there is nothing on the underlying screen. This code runs as soon as it is loaded, and the rest of the page has yet to be delivered. Since our plan is to tinker with that page, we really don’t want our code to be running just yet. We will need to wait to make sure that the rest of the page is there as well before we attempt to alter it. We can accomplish that with a little recursive loop that will look for an important field such as the file to be uploaded, and until that element is present, just loop back and check again. Here is a modified version of the script that will accomplish that.

if (window.location.pathname == '/upload.do' && window.location.search.startsWith('?attachment_id=')) {
	waitForPageLoad();
}

function waitForPageLoad() {
	if (document.getElementById('attachFile')) {
		installApplication();
	} else {
		setTimeout(waitForPageLoad, 100);
	}
}

function installApplication() {
	alert('So far, so good ...');
}

If that works as intended, the alert should not pop until at least the parts of the page in which we are interested have arrived.

Second test of the new global UI Script

That’s better. Now at least the stuff that we want to play with is all present in the DOM. The first thing that we will want to do is to hide the original form and then replace it with some kind of message indicating that things are happening in the background and there is nothing for the operator to do right at the moment. Here is a little code that will find the DIV that contains the major components, hides it, and replaces it with something else.

var originalContent = document.getElementsByClassName('section-content')[0];
originalContent.style.visibility = 'hidden';
var newContent = document.createElement('div');
newContent.innerHTML = '<h4 style="padding: 30px;">&nbsp;<img src="/images/loading_anim4.gif" height="18" width="18">&nbsp;Uploading Update Set XML file ...</h4>';
originalContent.parentNode.insertBefore(newContent, originalContent);

There are a couple of things to note on the above code. For one, DOM manipulation is frowned upon in the ServiceNow environment. You will get tagged for that in an Instance Scan as a bad practice, and you should really try to avoid doing things like that if at all possible. Still, sometimes you have to break the rules to get something done; there is a reason that this site is called ServiceNow Hackery and not ServiceNow By The Book. Sometimes you have to step outside of the lines in order to do what you want to do. But again, this should be a last resort and not adopted as a routine way of doing things. The other thing to note is the use of the innerHTML method. Again, the preferred way of doing things would be to create each DOM node individually, set all of the appropriate values on each node, and then link them all up to each other before inserting them into the active DOM. That’s the way that it should be done, but I was just too lazy to go through all of that and I took the easy way out instead. But that’s another thing to which folks might take exception in certain circles.

To test all of this out, we can go back to our version page and click on the new Install button one more time.

Third test of the new global UI Script

With all of that basic housekeeping out of the way, we can now focus on what we are here for. The first thing that we need to do in order to accomplish our goal is to pull down the Update Set details using GlideAjax to access the Script Include that we created last time. Before we do that, though, we need to snag the attachment record sys_id from the URL parameter. With that in hand, we can then make our Ajax call.

var attachmentId = window.location.search.substring(15);
var ga = new GlideAjax('x_11556_col_store.ApplicationInstaller');
ga.addParam('sysparm_name', 'getXML');
ga.addParam('attachment_id', attachmentId);
ga.getXMLAnswer(submitForm);

Now we just need to build a submitForm function that will parse the returned JSON string to access the file name and file contents, and then somehow use that as if it were a file on the local system so that we can submit the form. That sounds like a bit of work in and of itself, and I’m still not exactly sure how I am going to pull that off, so let’s save that exercise for our next exciting installment.

Collaboration Store, Part XLVII

“Relinquish your attachment to the known, step into the unknown, and you will step into the field of all possibilities.”
Deepak Chopra

While we wait patiently for the testing results of the instance sync process to come trickling in, we should go ahead and get started on the application installation process. As I laid out earlier, there are a couple of different ways to go here, and I really do not like either one. However, as much as I would like to have come up with a preferable third alternative, nothing has really come to mind, despite the fact that I have been dragging out the instance sync process for much longer than I should have while I searched in vain for a better solution. We have to keep pushing forward, though, so enough waiting. Let’s get into it.

I have decided to go with the client-side approach, mainly because I don’t want to get involved with the security issues related to attempting to emulate a user session on the server side. Both approaches involve quite a bit more hackery than I really care to include in something that is supposed to be a viable product to be distributed and used by others, but at this point, I don’t see any other way. The client-side approach involves downloading the Update Set XML file from the server, and then presenting it back as if it were a local file uploaded to the import XML process. So, to start with, we will need a Script Include function that will deliver the XML to the client side. Since this will be a client callable Script Include, we will want to create a new one rather than add another function to our existing utilities. We will call this new Script Include ApplicationInstaller, and check the Client callable checkbox when we create it. Our lone function will be called getXML and it will accept a single parameter for the sys_id of the attachment record, and then use that sys_id to fetch the record and return both the file name and the file contents in a JSON string response.

var ApplicationInstaller = Class.create();
ApplicationInstaller.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

	getXML: function() {
		var answer = {};
		var sysId = this.getParameter("attachment_id");
		if (sysId) {
			var sysAttGR = new GlideRecord('sys_attachment');
			if (sysAttGR.get(sysId)) {
				var gsa = new GlideSysAttachment();
				answer.fileName = sysAttGR.getDisplayValue('file_name');
				answer.xml = gsa.getContent(sysAttGR);
			}
		}
		return JSON.stringify(answer);
	},

    type: 'ApplicationInstaller'
});

There’s nothing too extraordinary about the code here. We do leverage our old friend the GlideSysAttachment to grab the contents of the file, but that’s nothing that we have not already done before. Other than that, it’s pretty basic stuff.

Now that we have a way to fetch the name and contents of the attached file, we will need a way to launch the process, which will either be a clone of the existing upload.do page or the original upload.do page with a little creative hackery to bend it to our will. I hate to mess with the original, but I also don’t want to create an entire second copy if I don’t have to. But let’s not get too far ahead of ourselves just yet. Right now, we just need to build a launcher for the process, and we can easily change the page that we launch once we decide which way to go.

To launch the installation process, we can add a UI Action to the version form, which we can simply label Install. We will want to make this action conditional, since we would not want to see this option on a version that has already been installed. At this point, we don’t need to worry about the code behind the action; we can just create it and see how it renders out on the page.

New UI Action “Install”

After saving the new action, we can pull up a version page and see how it looks.

New UI Action rendered on the version page

Now we need to add some code to make the action actually do something. Basically, we just want to jump over to whatever version of the upload.do page we end up pursuing, but before we do that we need to include a parameter. The client callable Script Include that we just created takes an attachment record sys_id as a parameter, but out action is sitting on the version record, not the attachment record. We will need to hunt down the attachment record to pull out the sys_id and then we can pass that along to our destination so that it can be used to make the GlideAjax call to our Script Include function. Here is the UI Action script to make all of that happen.

var attachmentGR = new GlideRecord('sys_attachment');
attachmentGR.addQuery('table_name', 'x_11556_col_store_member_application_version');
attachmentGR.addQuery('table_sys_id', current.sys_id);
attachmentGR.addQuery('content_type', 'CONTAINS', 'xml');
attachmentGR.query();
if (attachmentGR.next()) {
	action.setRedirectURL('/upload.do?attachment_id=' + attachmentGR.getUniqueValue());
} else {
	gs.addErrorMessage('No Update Set XML file found attached to this version');
}

Once again, this is pretty basic stuff that doesn’t require a whole lot of explanation. We query the attachment table for an XML file attached to the current record, and if we find it, we construct a URL and jet off to that page. If not, we throw up an error message, but that really shouldn’t happen unless something has gone horribly wrong somewhere along the line.

So now we have built enough parts to get us to the page that will actually fetch the XML from the server and then push it back up again. I need to make a decision as to whether I should hack up the original upload.do page or just use it as a guide to make one of my own. I’ll need to give that some thought, so let’s pause for now and we’ll jump into that next time out.

Collaboration Store, Part XXXIV

“The key is not to prioritize what’s on your schedule, but to schedule your priorities.”
Stephen Covey

So far, we have completed the first two of the three primary components of the project, the initial set-up process and the application publication process. The last of the three major pieces will be the process that will allow you to install an application obtained from the store. Before we dive straight into that, though, we should pause to take a quick look at what we have, and what still needs to be done in order to make this a viable product. At this point, you can install all of the prerequisites and then grab the latest Update Set, install it, and go through the set-up process to create either a Host or Client instance. Once you get through all of that, you are ready to publish any local Scoped Application to the store, which will then be shared with all other instances in your Collaboration Store community.

What you cannot do, just yet, is to find an application published to the store by some other instance and install it on your own instance. That’s the missing third leg of the stool that we will need to take on next. But that is not all that is left to be done. Once we get the basics to work, there are quite a number of other things to address before one could consider this to be truly usable. Some things are just annoyances, but others are definite features that you would have to consider essential for a complete product.

Speaking of annoyances, one of the things that I really don’t like is that when you publish an app to XML for distribution, the resulting Update Set XML does not include the app’s logo image. Clearly it is a part of the app, and if you push an app to an internal store and pull it down into another instance, it comes complete with the logo, so why they left that out of the XML is a mystery to me. I don’t like that for a couple of reasons: 1) when you pull down the XML for this app, you do not get the logo, and 2) when we use the XML to publish an app to the store, the logo is missing there as well. I have seen people complain about this, but I have not, as yet, seen a solution. I would really like to address that, both for my own apps as well as for the process that we are using in this one.

Speaking of logos, another feature that I would like to have is to provide the ability for each instance to have its own distinctive logo image, so that everything from that particular instance could be tagged with that image as a way to visually identify where the app originated. That’s not a critical feature, which is why I did not include it initially, but it has always been something that I felt should be a part of the process, particularly when you start thinking about ways to browse the store and find what you are looking for. That’s definitely on the We-will-get-around-to-it-one-day list.

Browsing the store is another thing that will need some attention at some point. Right now, we just want to prove that we can set-up the app, publish an application, and install an application published by someone else. Those are the fundamental elements of the app. But once we get all of that worked out, being able to hunt through the store to find what you want will be another important capability to build out. We’re not done with the fundamentals just yet, so we don’t want to put too much energy into that issue right at the moment, but at some point, we will need to create a user-friendly way to easily find what you need.

That, of course, leads into things like searchable keywords, tags, user ratings, reviews, and the like, but now is not the time to head down that rabbit hole. Still, there are a lot of possibilities here, and this could turn into a life-long project in and of itself. That’s probably not a good thing, though!

Anyway, we won’t get anything done if we don’t focus, so we need to stay on task and figure out the application installation process. Once again, there are several options available, but the nicest one seems to be the process that you go through to install an app from an internal store. That’s basically a one-click operation and then the app is installed. Unfortunately, that particular page is neither Jelly nor AngularJS, so you can’t just peek under the hood and see the magic like you can with so many other things on the Now Platform. Another option would be to hack up a copy of the Import XML Action on the Update Set list page to push in the attached XML from a published app version, but that only takes things so far; you still have to Preview the Update Set, resolve any issues, and then manually issue the Commit. It would be much nicer if we could just push a button and have the app installation process run in the background and notify you when it was completed. Obviously, we have some work to do here to come up with the best way to go about this, and we had better figure that out relatively soon. Next time, if we are not dealing with test results from the last release, we will need to start building this out.

Collaboration Store, Part XXX

“Writing the first 90 percent of a computer program takes 90 percent of the time. The remaining ten percent also takes 90 percent of the time and the final touches also take 90 percent of the time.”
Neil J. Rubenking

Well, the test results are starting to pour in, and it looks like I screwed things up when I manually edited the Update Set in an effort to eliminate one of the errors reported in the earlier testing cycle. It seems as if I should have left well enough alone and just informed people to ignore the error if it comes up. Here is an unmolested Update Set that should not have the problem that I created by hacking up the earlier version after it was generated. Hopefully, this will resolve that issue.

Two things to note, then, of this new version: 1) if you happen to get the unfortunate Table ‘sys_hub_action_status_metadata’ does not exist error, just ignore it, and 2) if you get any preview errors related to any sys_properties, be sure to skip those updates, as you do not want to overlay the property values that were established during the set-up process.

One of the other things that was reported was that it was not really clear as to what, exactly, needed to be tested. I have a tendency to focus on the construction process exclusively, without a lot of attention to the actual end product itself, so let’s see if we can’t rectify that situation a little bit now.

The initial early release of this effort was focused on the set-up process. All the set-up process does is set you up as the Host instance, or get you registered with a Host instance if you are setting up a Client instance. That’s all that it did, so the testing was limited to attempting to set up a Host, and then attempting to set up one or more Clients. A successful test would have all instances appear in the instance table on every instance involved in the testing. That seemed to be pretty straightforward.

For this next iteration, we introduced the ability to actually publish a Scoped Application to the Host. To test this newest feature, you will first have to have gone through the set-up process successfully, and then you need an app to publish. Any app in development will do, and if you don’t have one, you can always just stub one out for the testing.

To publish an app, you need to bring up the app’s primary form, and if all went well with the installation, there should be a new UI Action down at the bottom of the page called Publish to Collaboration Store.

New Publish to Collaboration Store UI Action

If you click on that guy and follow the ensuing pop-up dialogs through completion, the app should be published. To verify that all went well, you will have to go over to the Host instance and see if the app actually appears in the Related List under the publisher’s instance record. You should verify the presence of the application record, the version record, and the XML Update Set attachment. If all of those things are present, then the app was successfully published to the Host.

The one thing that will not happen just yet is for the newly published app to be distributed to any of the other Client instances in the community. That process is still under development, and is not included in this version of the application. Once that gets completed and all of the issues from this round of testing get resolved, we will put out yet another beta test version and go through the testing process all over again.

Thanks again to all of you who are taking the time to take this out for a spin. Any and all feedback is greatly appreciated. Please feel free to report any issues or successes in the comments below. Next time, if there are no further results to review, we will take a look at building out the distribution of newly published versions to the rest of the instances served by the Host.

Collaboration Store, Part XXI

“Slow, steady progress is better than daily excuses.”
Robin Sharma

Now that we have managed to create an Update Set from our UI Action, it is time to figure out how to best accomplish the rest of the tasks needed to complete the publication of our Scoped Application to the Host instance. We will need another pop-up window to control the rest of the activities and to report the status to the operator, as the original pop-up went away when the progress bar appeared, and the progress bar goes away once you click on the Done button (an unnecessary click for our purposes, which I would like to avoid at some point, but for now, we will leave it alone and focus on other, more important activities). The next pop-up window should be the last, as it will be a fully custom component and we should be able to control all of the events from this point forward.

Here is what I am thinking at this point: We can create a simple UI that lists all of the steps necessary to complete our objective, and reveal them one at a time as we progress through the work involved. We can have three different icons ahead of each item on the list, one for in progress, one for success, and another for failure, and then show/hide the icons based on the current status of each step in the process. To make that work, we would have to contact the server side for each step along the way, updating the UI on the client side with the status returned from each server call. Once we successfully complete the final step, we can reveal a Done button to close the pop-up.

To find some appropriate icons, I entered image_picker.do in the left-hand navigator to bring up the list of all of the images currently in the /images/ directory of the platform.

image_picker.do results

I selected the following icons to represent the associated stages for each step:

  • In progress: /images/loading_anim4.gif
  • Complete: /images/check32.gif
  • Error: /images/delete_row.gif

As with many things on the Now Platform, there are many ways in which we can develop a pop-up dialog. I am most comfortable with building Service Portal widgets, and I have used widgets for UI pop-ups before, so that was a tempting way to go. However, the initial pop-up that we customized for our UI Action was a straight UI Page, so I decided that, if I wanted to keep things consistent, I should build this new pop-up the same way. So I cloned our original publish_app_dialog_cs UI Page to create publish_app_dialog_cs_2. Then I went in to our original UI Page and removed this code:

var notification = {"getAttribute": function(name) {return 'true';}};
CustomEvent.fireTop(GlideUI.UI_NOTIFICATION + '.update_set_change', notification);
window.location.href = "sys_update_set.do?sys_id=" + updateSetId;

… which took you to the newly created Update Set when everything was finished, and replaced it with this:

var dialogClass = GlideModal ? GlideModal : GlideDialogWindow;
var dd2 = new dialogClass("x_11556_col_store_publish_app_dialog_cs_2");
dd2.setTitle(new GwtMessage().getMessage('Publish to Collaboration Store'));
dd2.setPreference('sysparm_mbr_app_id', this.mbrAppId);
dd2.setPreference('sysparm_app_sys_id', this.appId);
dd2.setPreference('sysparm_upd_set_id', updateSetId);
dd2.setWidth(500);
dd2.render();

… which now pops up our newly cloned UI Page in a new modal dialog. Now we actually have to build out the page. To begin, as I usually do, I started with the HTML of the presentation layer, which came out like this:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="true" xmlns:j="jelly:core" xmlns:g="glide" xmlns:g2="null">

<g2:evaluate jelly="true">

var existingVersions = '';
var mbrAppId = jelly.sysparm_mbr_app_id;
var appSysId = jelly.sysparm_app_sys_id;
var updSetId = jelly.sysparm_upd_set_id;
var appAction = 'Updating';
if (mbrAppId == 'new') {
	appAction = 'Creating';
}

</g2:evaluate>

<g:ui_form>
	<style type="text/css">
		#publish_to_update_set_dialog_container input {
			width: 100%;
			margin-bottom: 10px;
		}
		#publish_to_update_set_dialog_container label {
			text-align: right;
		}
	</style>
	<div id="publish_to_collaboration_store_dialog_container">
		<div class="modal-body">
			${gs.getMessage('Completing the process of publishing the application to the Collaboration Store')}
			<input id="mbr_app_id" type="hidden" value="$[mbrAppId]"/>
			<input id="app_sys_id" type="hidden" value="$[appSysId]"/>
			<input id="upd_set_id" type="hidden" value="$[updSetId]"/>
		</div>

		<div style="padding-left: 50px;">
			<div class="row" id="phase_1">
				<image id="loading_1" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_1" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_1" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 5px; font-weight:bold;">
					Converting Update Set to XML
				</span>
			</div>
			<div class="row" id="phase_2" style="visibility: hidden; display: none;">
				<image id="loading_2" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_2" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_2" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					$[appAction] the Application record
				</span>
			</div>
			<div class="row" id="phase_3" style="visibility: hidden; display: none;">
				<image id="loading_3" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_3" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_3" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					Creating the Version record
				</span>
			</div>
			<div class="row" id="phase_4" style="visibility: hidden; display: none;">
				<image id="loading_4" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_4" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_4" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					Sending the Application record to the Host instance
				</span>
			</div>
			<div class="row" id="phase_5" style="visibility: hidden; display: none;">
				<image id="loading_5" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_5" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_5" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					Sending the Version record to the Host instance
				</span>
			</div>
			<div class="row" id="phase_6" style="visibility: hidden; display: none;">
				<image id="loading_6" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_6" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_4" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					Sending the Update Set XML to the Host instance
				</span>
			</div>
		</div>
	</div>
</g:ui_form>
</j:jelly>

Basically, it is the same block of code repeated 6 times for the six remaining tasks needed to complete the process. Each item in the list contains a description of the step along with our three icons, only one of which should appear at any given time. Initially, the in progress icon is visible as soon as the step becomes visible, and my plan is to hide that one and reveal one of the other two, depending on how things came out once the step has been completed.

Here is how it looks when it first appears:

New pop-up dialog box

That takes care of the easy part. Now we have to put all of the code underneath of the UI to actually do all of the things that need to be done. That’s quite a bit of work, so let’s say we will get started on that next time out.

Collaboration Store, Part XX

“It’s always too soon to quit!”
Norman Vincent Peale

Now that we have the needed global Script Include out of the way, we can turn our attention to building the Publish to Collaboration Store UI Action. Before we do that, though, we are going to need a couple more tables, one for the published applications, and another for the application versions. I won’t waste a lot of time here on the process of building a new table; that’s pretty basic Now Platform stuff that you can find just about anywhere. I also did not spend a lot of time creating the tables. At this point, they are very basic and just enough to get things going. I am sure at some point I will be coming back around and adding more valuable fields, but for right now, I just included the bare necessities to create this initial publication process.

Here is the form for the base application table:

Base Member Application table input form

… and here is the form for the application versions:

Member Application Version table input form

There is an obvious one-to-many relationship between the application record and all of the various version records for an application. In practice, all of the versions would be listed under an application as a Related List on the application form. We can show examples of that once we build up some data.

Now that that is done, we can get back to our UI Action. Since our action is going to be very similar to the stock Publish to Update Set… action, the easiest way to create ours is to pull up the stock action and use the context menu to select Insert and Stay to clone the action.

Select Insert and Stay from the context menu to clone the UI Action

Now we just have to rename the action and update the script, which actually turned out to be more modifications that I had anticipated, primarily because our action is not in the global scope. First, we had to rename the function to avoid a conflict with the original action with which we will be sharing the page. Also, the gel function and the window object are not available to scoped actions, and of course, we needed to point to our own UI Page instead of the original and put in our own title.. Here is the modified script:

function publishToCollaborationStore() {
	var sysId = g_form.getUniqueValue();
	var dialogClass = GlideModal ? GlideModal : GlideDialogWindow;
	var dd = new dialogClass("x_11556_col_store_publish_app_dialog_cs");
	dd.setTitle(new GwtMessage().getMessage('Publish to Collaboration Store'));
	dd.setPreference('sysparm_sys_id', sysId);
	dd.setWidth(500);
	dd.render();
}

I also removed all of the conditionals for now, since some of that code was not available to a scoped UI Action, and we will end up having our own conditions, anyway, with which we can deal later on. Right now, I am still just focused on seeing if I can get all of this to work.

The next thing that we need to do is to build our own UI Page, on which we can also get a head start by cloning the original using the same Insert and Stay method. The modifications here are a little more extensive, as we want to do much more than just create an Update Set. One thing that we will want to do is check to make sure that the version number is not one that has already been published. To support that, I added some code at the top to pull all of the current versions so that I can stick them into a hidden field.

var existingVersions = '';
var mbrAppId = 'new';
var appSysId = jelly.sysparm_sys_id;
var appGR = new GlideRecord('sys_app');
appGR.get(appSysId);
var appName = appGR.getDisplayValue();
var appVersion = appGR.version || '';
var appDescription = appGR.short_description;
var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
if (mbrAppGR.get('application', appSysId)) {
	mbrAppId = mbrAppGR.getUniqueValue();
	var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
	versionGR.addQuery();
	versionGR.query('member_application', mbrAppId);
	while (versionGR.next()) {
		existingVersions += '|' + versionGR.getDisplayValue('version');
	}
	existingVersions += '|';
}

To pass this value to the client side for validation, I added a hidden field to the HTML for the page:

<input id="existing_versions" type="hidden" value="$[existingVersions]"/>

To check the validity on the client side, I retained the original version validity check and then added my own check of the existing versions right underneath using the same alert approach to delivering the bad news:

this.existingVersions = gel('existing_versions').value;
...
var vd = g_form.validators.version;
if (!vd)
	console.log("WARNING.  Cannot find validator for version field in parent form.");
else {
	var answer = vd.call(g_form, this.versionField.value);
	if (answer != true) {
		alert(answer);
		return null;
	}
}
if (this.existingVersions.indexOf('|' + versionField.value + '|') != -1) {
	alert('Version ' + versionField.value + ' has already been published to the Collaboration Store');
	return null;
}

The last thing that we have to change is what happens once the Update Set has been created. In the original version that we cloned, once the Update Set has been produced, the operator is then taken to the Update Set form to see the newly created Update Set. We don’t want to do that, as we still have much work to do to, including the following:

  • Update or Create an Application record
  • Create a new Version record linked to the Application record
  • Convert the Update Set to XML
  • Attach the XML to the Version Record
  • Send all of the updated/created records over to the Host instance, including the attached XML

We will need some kind of vehicle in which to perform all of the these tasks and keep the operator updated as to the progress and success or failure of each one of the steps. That sounds like quite a bit of work, so I think that will be a good topic for our next installment.

Collaboration Store, Part XIX

“I like taking things apart and putting them back together. Tinkering. I’d be a professional tinkerer. Tinkerbell. I think that’s what they’re called.”
Chris Carmack

When we took a peek under the hood of the two UI Actions that produced an Update Set from a Scoped Application and turned it into an XML file, it looked like we could leverage most of the code to accomplish our own objective of publishing an app to the Host instance. Unfortunately, after experimenting a bit with various modifications, it appears that some things simply cannot be done within the scope of our Collaboration Store app. Two of the Script Includes involved can only be invoked from another global component (UpdateSetExport and ExportWithRelatedLists), and the delete statement that is executed after the XML file is produced is also something that can only be done in the global scope. I did not really want to create any components outside of our application scope, but in this instance, it doesn’t look like I have any other option.

Given this new tidbit of information, my new goal is to create a single global component and limit the contents of that component to just the things that are absolutely necessary to be in the global scope. I may even try to create this component in the global scope programmatically as part of the set-up process so that a separate global Update Set is not needed, but for now, let’s just see if we can get it to work. I called my new component CollaborationStoreGlobalUtils, and set the Accessible from to All application scopes so that it could be called from any of our scoped components.

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

	type: 'CollaborationStoreGlobalUtils'
};

All of the things that needed to be in the global scope seemed to be centered around the process to convert an Update Set to an XML file, so I created a function called updateSetToXML to which I passed the GlideRecord for the Update Set. The purpose of the function was to turn the Update Set into XML and return the XML text. Most of the code I lifted straight out of the Publish to XML UI Action and the Processor that it called.

updateSetToXML: function(updateSetGR) {
	var updateSetExport = new UpdateSetExport();
	var rusSysId = updateSetExport.exportUpdateSet(updateSetGR);
	var remoteUpdateGR = new GlideRecord('sys_remote_update_set');
	remoteUpdateGR.get(rusSysId);
	var exporter = new ExportWithRelatedLists('sys_remote_update_set', rusSysId);
	exporter.addRelatedList('sys_update_xml', 'remote_update_set');
	var fakeResponse = this.responseObject();
	exporter.exportRecords(fakeResponse);
	remoteUpdateGR.deleteRecord();
	return fakeResponse.getOutputStreamText();
}

This is an abbreviated version of the code lifted from the stock components, as I removed things like role checking, the validity checking of various GlideRecords, and the value of passed parameters. My thinking is that those things can all be handled in our scoped components before we get to this point, so this function only needs to contain those things that are required to be in the global scope. Since the code that we are borrowing was intended to be used to send the XML file created to the servlet response output stream, I did have to introduce a little bit of creative hackery to replace the expected g_response object parameter in the exportRecords function with an object of my own design.

To construct an object that would be accepted as valid input to the exportRecords function, I dug through the code in that function looking for all of properties and methods that it was expecting to find. Most of those were basic setter functions used to define the HTTP response, and since we weren’t actually going to have an HTTP response, I was able to just include those in the object as empty functions that did nothing and returned nothing.

addHeader: function() {
},
setHeader: function() {
},
setContentType: function() {
},

The one method that did expect a return was the getOutputStream function, and it was expected to return a valid Java output stream object. Back in my Java developer days, whenever I needed to convert an output stream to a string, I always used the java.io.ByteArrayOutputStream class, so I set up a local variable that was an instance of that class and then added a getter to return that variable.

outputStream: new Packages.java.io.ByteArrayOutputStream(),
getOutputStream: function() {
	return this.outputStream;
}

Utilizing the global Packages object is another thing that cannot be done in a Scoped Application, but since we already made the commitment to build this global Script Include, that didn’t turn out to be a problem here. The last thing that I needed to do was to convert the output stream object to a string once the XML was assembled, so I created this additional nonstandard function to obtain the text:

getOutputStreamText: function() {
	var dataAsString = Packages.java.lang.String(this.outputStream);
	dataAsString = String(dataAsString);
	return dataAsString;
}

That’s the entire fake g_response object, and the function that provides the object looks like this:

responseObject: function() {
	return {
		outputStream: new Packages.java.io.ByteArrayOutputStream(),
		addHeader: function() {
		},
		setHeader: function() {
		},
		setContentType: function() {
		},
		getOutputStream: function() {
			return this.outputStream;
		},
		getOutputStreamText: function() {
			var dataAsString = Packages.java.lang.String(this.outputStream);
			dataAsString = String(dataAsString);
			return dataAsString;
		}
	};
}

So far, those are the only two functions that I have had to include in my global component. The entire thing at this stage of the process now looks like this:

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

	updateSetToXML: function(updateSetGR) {
		var updateSetExport = new UpdateSetExport();
		var rusSysId = updateSetExport.exportUpdateSet(updateSetGR);
		var remoteUpdateGR = new GlideRecord('sys_remote_update_set');
		remoteUpdateGR.get(rusSysId);
		var exporter = new ExportWithRelatedLists('sys_remote_update_set', rusSysId);
		exporter.addRelatedList('sys_update_xml', 'remote_update_set');
		var fakeResponse = this.responseObject();
		exporter.exportRecords(fakeResponse);
		remoteUpdateGR.deleteRecord();
		return fakeResponse.getOutputStreamText();
	},

	responseObject: function() {
		return {
			outputStream: new Packages.java.io.ByteArrayOutputStream(),
			addHeader: function() {
			},
			setHeader: function() {
			},
			setContentType: function() {
			},
			getOutputStream: function() {
				return this.outputStream;
			},
			getOutputStreamText: function() {
				var dataAsString = Packages.java.lang.String(this.outputStream);
				dataAsString = String(dataAsString);
				return dataAsString;
			}
		};
	},

	type: 'CollaborationStoreGlobalUtils'
};

Now that we have that little piece out of the way, we can start building our Publish to Collaboration Store UI Action and the associated UI Page that will be launched in the modal pop-up when that action is selected. Unless we have some additional feedback from the set-up process testing, we’ll jump right into that next time out.

Collaboration Store, Part XVII

“I believe in a world where all these things can happen, even if I have to do them myself.”
Adrian Lamo

While we wait for the hordes of fellow developers currently running their extensive tests on the initial version of the Collaboration Store app to report their findings, I thought it might be a good time to give a little bit of thought as to where things should go from here. As I mentioned earlier, there are a number of other improvements that I would like to make to the set-up process once we are sure that everything is working as it should. That would definitely be one specific area towards which we could direct our attentions next. On the other hand, I am quite anxious to see if I can actually do some real sharing of artifacts between instances, and I am not quite sure how I am going to do that, so I would like to start focusing on that little project as well. I have managed to cobble together enough parts to get multiple instances aware of one another, so even though the initial set-up process is not quite ready for prime time, maybe it is good enough for now and I can start trying to work towards accomplishing the actual purpose of this application. Besides, we have not received any results from any of those selfless volunteers who have been doing the outside testing as yet, so we really don’t want to disturb that code at this point.

I would like to share all kinds of different artifacts at some point, but to start out, I am going to focus on sharing Scoped Applications in much the same way that applications are shared to internal app stores or the public ServiceNow App Store. There are two sides to the process of sharing: 1) publishing your app to the desired target, and 2) retrieving an app from the desired source and installing it in your instance. Since I can only do one thing at a time, and one obviously has to take place before you can do the other, my initial focus will be to see if I can publish an app to the Host instance. Once we clear that hurdle, then maybe we can see if we can do the flip side and pull something down from the Host and get it installed. But that’s nothing that we need to worry about today, particularly since we don’t even know yet if we can pull off putting something out there!

The first thing that we will need is a couple of tables, one to store the published applications and another to store each version of the application. These are just meta data tables containing information about the app and the versions. I envision that the application itself will be an XML Update Set file, which I plan to attach to the application version table record for each version published. The question is, How do we do that?

There are already built-in capabilities to turn an app into an Update Set and to turn an Update Set into an XML file. Both of those are UI Actions, so taking a peek under the hood of those artifacts would seem like a good place to start. Before you can turn an Update Set into an XML file, you first have to have an Update Set, so let’s first take a peek at the code under the UI Action that does that. The easiest way to do that would be to pull up a Scoped Application and then use the hamburger menu to pull up the list of UI Actions for that form.

Select Configure -> UI Actions from the context menu to pull up the list of UI Actions

The one that we are looking for is called Publish to Update Set… and what we are looking for is the code that is executed when the action is selected.

function publishIt() {
	var sysId = gel('sys_uniqueValue').value;	
	var dialogClass = window.GlideModal ? GlideModal : GlideDialogWindow;
	var dd = new dialogClass("publish_app_dialog");
	dd.setTitle(new GwtMessage().getMessage('Publish to Update Set'));
	dd.setPreference('sysparm_sys_id', sysId);
	dd.setWidth(500);
	dd.render();
}

Well, that didn’t tell us much. All this code does is launch a dialog box. We will need to look at the UI Page that will appear in the pop-up window, which we can see from the code is called publish_app_dialog. The key element there is the publishApp() function in the Client script of that UI Page.

function publishApp(updateSetId) {
	var dd = new GlideModal("hierarchical_progress_viewer");
	dd.on("beforeclose", function () {
               var notification = {"getAttribute": function(name) {return 'true';}};
               CustomEvent.fireTop(GlideUI.UI_NOTIFICATION + '.update_set_change', notification);
               window.location.href = "sys_update_set.do?sys_id=" + updateSetId;
	});

	dd.setTitle("Progress");
	dd.setPreference('sysparm_function', 'publishToUpdateSet');
	dd.setPreference('sysparm_update_set_id', updateSetId);
	dd.setPreference('sysparm_sys_id', this.appId);
	dd.setPreference('sysparm_name', this.inferredUsName);
	dd.setPreference('sysparm_version', this.versionField.value);
	dd.setPreference('sysparm_description', this.descriptionField.value);
	dd.setPreference('sysparm_include_data', this.includeDataField.checked);
	dd.setPreference('sysparm_progress_name', "Publishing application");
	dd.setPreference('sysparm_ajax_processor', 'com.snc.apps.AppsAjaxProcessor');
	dd.setPreference('sysparm_show_done_button', 'true');

	dd.render();
	GlideDialogWindow.get().destroy();

	return dd;
}

Basically, this code replaces the original pop-up with a progress bar driven off of the server side publishToUpdateSet function of the com.snc.apps.AppsAjaxProcessor. This is probably something that we still want to do. We just don’t want to be sent to the Update Set form when it all over. Instead, we want to go ahead and turn it into an XML file, and then we want to attach that file to a new version record for the application. Also, if the application has never been published before, we will need to create the master application record as well. But it looks like we can steal most of this code so far. We will need to clone both the UI Action and the UI Page, and then point our cloned action to our cloned page, which is where we will make the majority of the changes. So far, so good.

Now we need to turn the Update Set into XML. For that, we take a peek at the other UI Action for that purpose, found on the Update Set form. To hunt that guy down, we pull up any local Update Set and use the same hamburger menu options to pull up the UI Actions for that form and look for the one called Export to XML. Here is the relevant script:

var updateSetExport = new UpdateSetExport();
var sysid = updateSetExport.exportUpdateSet(current);

action.setRedirectURL("export_update_set.do?sysparm_sys_id=" + sysid + "&sysparm_delete_when_done=true");

The first couple of lines look important, but that last one looks like it pops up the XML file for downloading, which we definitely do not want to do in our adaptation. It turns out that export_update_set is a Processor, which also has code of its own, so we better dig into that guy and see if does anything important that we might need to retain. We can find that guy by entering the word processors in the left-hand navigation menu and then select the lone menu item that appears to bring up the list. Once we have the list, we just need to search for the one where the Path is export_update_set. Here is the relevant code:

(function process(g_request, g_response, g_processor) {
    var sysid = g_request.getParameter('sysparm_sys_id');
    var remoteUpdateGR = new GlideRecord('sys_remote_update_set');
    if (!remoteUpdateGR.get(sysid)) {
        gs.addErrorMessage(gs.getMessage('Update Set is invalid'));
        return;
    }
    if (!gs.hasRole('admin') && !sn_app_api.AppStoreAPI.canPublishToUpdateSet(remoteUpdateGR.application)) {
        gs.addErrorMessage(gs.getMessage('You do not have permission to perform this operation'));
        g_response.setStatus(403);
        return;
    }
    var exporter = new ExportWithRelatedLists('sys_remote_update_set', sysid);
    exporter.addRelatedList('sys_update_xml', 'remote_update_set');
    exporter.exportRecords(g_response);
    var del = g_request.getParameter('sysparm_delete_when_done');
    if (del == "true" && remoteUpdateGR.canDelete())
        remoteUpdateGR.deleteRecord();
})(g_request, g_response, g_processor);

So it looks like code in which we are interested is the stuff going on with the variable exporter. Unfortunately, there is no output from the exportRecords function of that object and it takes as input the global g_response object. That will send the output to the browser, which we definitely do not want to do. Maybe we can hack it by sending it a fake g_response object, and then pull our output from there. If you right click on the class name (ExportWithRelatedLists), you can use the resulting context menu to pull up the definition and see exactly what it does. There is quite a bit of code in there, but here is the relevant function:

exportRecords: function(response){
    this.setHeaders(response);
    var outputStream = response.getOutputStream();
    this.hd = this.beginExport(outputStream);
    var gr = new GlideRecord(this.parent_table);
    gr.get(this.sys_id);
    this.exportRecord(gr);
    this.exportChildren();
    this._exportQuerySets();
    this.endExport(outputStream);
}

It basically sends everything to the outputStream that it obtains from the response object that you send it. So, we could build our own response object with our own outputStream, use this component right out of the box, and then grab the outputStream from our phony g_response object. Or, we just steal the code from this guy and use it to build our own exporter and have our version of the exportRecords function simply return the XML. Either way, we should be able to leverage all of this code to do what it is that we want to do. That should give us our XML file, anyway.

Of course, generating the XML from the Scoped Application is just the start of the process. Once we have it in hand, we have to create a version record, attach the XML to the version record as a file, and then ship the thing over to the Host instance. Once it arrives at the Host, we will also have to send the version record and the attached XML file out to all of the other instances. That’s all quite a bit of work, but so was the set-up process, and we just trudged through that effort one piece at a time until we got to the end of the to-do list. Hopefully, we can do the same here. We may jump right into that next time, if we don’t yet have any feedback from the set-up testing.

Collaboration Store, Part II

“Tell me and I forget. Teach me and I remember. Involve me and I learn.”
Benjamin Franklin

Now that I have gone and thrown the idea out there for all to see, it’s time to get to work and see if I can actually pull this off. To begin, I need to set up the Scoped Application, which is basically a repeat of what I went through to set up the Scoped Application for my little Webhooks project, so there is no need to repeat all of that here. Here is how it came out:

Initial Collaboration Store Scoped Application

With that out of the way, the next order of business is to create that first table in which to store all of the instances. Again, that is pretty standard stuff and not really worthy of a step by step walk-through of the process, but here is the associated form, which will give you an idea of the columns that I have selected at this point in the process:

Member Organization table input form

Now we have a place to store the information on the participating instances, so it’s time to build the initial set-up process that will populate this table. Before we dive into that, though, I should mention that when I set up the application, I also set up a few System Properties using the UI Action that I created for that purpose a couple of years ago.

UI Action to set up System Properties for a Scoped Application

That’s been a handy little tool that does a number of things under the hood, but we don’t need to get into all of that here. If you are interested in that for any reason, you can grab an Update Set from here. For this phase of the project, I came up with three properties that I think will be needed in order to do what I would like to do. That may change over time as I get more into the weeds, but for now, here is the list:

Initial System Properties for the Collaboration Store application

That should give us all of the artifacts that we should need to start working on the initial set-up process. As you might have noticed in the above screen shot, I have already created a menu item to launch the initial set-up process from the navigation side bar. Right now, you can also see some of the other menu options for the app, but sometime before things are ready for prime time, my plan is to make all other menu items inactive, and then once the set-up process has been completed, a final step in the process would activate all of the others and inactivate the set-up menu item. For now, though, you can see everything, and it will probably be that way for some time until we get much closer to the end of things.

As for the set-up process itself, there are a number of different ways to go here. I could build something the main UI, where the primary technology is Apache Jelly. I could also build a Service Portal widget, where the primary technology is AngularJS. Both of those are considered Old School at this point, though, and all of the cool kids are now using the Now® Experience UI Framework and the ui-component extension for application development. While that seems like the appropriate way to go, my personal skill set does not yet include mastery of that particular technology, and I don’t really feel like this project would be a good place to address that particular shortcoming in my technical expertise. Since the initial set-up is just one small part of this effort, I am going to take the easy way out and just build a simple widget.

Building a brand new widget starting with a blank canvas is a little bit of a project, though, so that seems like something worthy of its own dedicated installment. Rather than start on that here, let’s take that up next time out.