“Take a deep breath, pick yourself up, dust yourself off and start all over again.” — Frank Sinatra
Last time, we finished up the code for the function that runs the notice distribution process. Now we need to come up with the actual content of those notices, which should direct the recipient to some function where they can indicate the disposition of the artifacts to be reviewed. In its simplest form, this would be a binary choice between keeping or discarding each item on the review list. However, there may be other dispositions for certain configurations, and there may be a need for additional options such as This item is no longer my responsibility or Remove this account only if it has been inactive for 90 days. To support such custom responses to a review request, we would need to add yet another table to our application to store the configured response options and link them to the configuration record. This adds a little bit of complexity to the process, but I think it would be worth doing to make things as flexible and useful as possible, so let’s go ahead and build that table now.
We will call our new table Review Statement and give it four fields, the reference to the configuration, an order, and a description and short description. We will want to link this as a related table on the configuration form so that statements can be easily added when setting up the configuration.
With that out of the way, we can start to visualize the form or page that the notice recipient would use to respond to the review notification. We could list the items to be reviewed down the page, and the configured choice across the top of the page, with a checkbox for each configured statement on each line containing an item. If there is more than one item to be reviewed, then a master checkbox at the top would also be helpful, so the recipient could simply check one box for the entire list of items. On the Now Platform, there are a number of different ways to construct such a page, but I am still partial to the Service Portal, so let’s build a Portal Widget for a Portal Page.
That was the plan, anyway.
Unfortunately, I did a really stupid thing before I got a chance to get started on that. It all started when I received a notice that my instance had some technical issues and that I needed to get rid of it and start over. Fair enough. I have had that particular instance for longer than I can remember, and I am sure that it was well past time to retire it and start all over with a new one. The notice said to be sure and back everything up before I wiped it out, but I have pretty much published every single thing that I have ever worked on, so I didn’t see any point in going through that. So I didn’t. I killed the old one and started over with a new one. Easy peasy.
What I neglected to consider was that this current project that I am working on right at the moment has not gotten far enough along for me to produce any public Update Sets, so there was no back up of everything that I have done so far. Oops! So now I have to go back and recreate all of the work that has been done so far, just to catch up to this point. I don’t mind doing things; in fact, I actually enjoy most of the stuff that I do here, and I mainly do it just for the fun of it. But I absolutely hate doing things twice. Now I just have to find the motivation to go back and do all of this again, just to get back to where I already was!
“On your darkest days do not try to see the end of the tunnel by looking far ahead. Focus only on where you are right now. Then carefully take one step at a time, by placing just one foot in front of the other. Before you know it, you will turn that corner.” — Anthon St. Maarten
Last time, we threw together the beginnings of a configuration script for Service Account dashboard using the Content Selector Configuration Editor. Now that we have a viable script, we need to create Service PortalPage that will utilize that configuration. To begin, we will pull up the list of Portal Pages and click on the New button to create a new page.
We will call our new page Service Account Dashboard and give it an ID of service_account_dashboard. Once we submit the form we can pull it back up and use the link down at the bottom of the form to bring it up in Service Portal Designer. Onto the blank canvas we will drag a 12-wide container, and beneath that one, we will drag in a 3/9 container. Into the upper 12-wide container, we will drag in the Dynamic Service Portal Breadcrumbs widget, and into the 3 portion of the 3/9 container, we will drag in the Content Selector widget. In the 9 portion of the 3/9 container, we will pull in the SNH Data Table from URL Definition widget. Now that we have placed all of the widgets, we will need to edit them, starting with the Content Selector.
Here is where we enter the full name of the configuration script that we created last time. Since this is a Scoped application, we need to include the scope with the name so that it can be successfully located. That’s all there is to configuring that widget, as most of the configuration information is contained in the referenced script. Configuring the Data Table widget is a little more involved.
Here we give it a title of Service Accounts and select an appropriate Glyph image. We check the Use Instance Title checkbox to get our title to show up, and we leave all of the rest of them unchecked. Once we save that and save the page, we should be ready to try it out, which we can do easily enough with the View page in new tab button up in the upper right-hand corner.
So far, so good. The default selection is active Service Accounts from the requester’s perspective, and you can see all of the account records from our failed and successful test submissions. I went ahead and retired one of them so that we could test the Retired state. Let’s click on the Retired button and see how that comes out.
That looks good as well. Now let’s try the Pending state, which should come up empty for the Service Account table, as pending requests have not gotten far enough along in the process to have created the record in that table yet.
Well, that’s not right! But you knew things were going too well at this point and it was about time for something to go horribly wrong. This is just a problem with our Filter, though, and should be easily remedied. We used the filter 1=0, which obviously did not work, so let’s try using an actual field from the table and do something like this in our config file:
filter: 'number=0',
Before we add that to all of the pending configurations, let’s pull up the dashboard again and see how that looks.
That’s better. Of course, to actually see the pending Service Accounts, we will need to add another table to our configuration. We can go back into the Content Selector Configuration Editor to do that, and then go back to the dashboard and check it out. That sounds like a good exercise for our next installment.
“We learn from failure, not from success!” — Bram Stoker
A while back I was working on my Collaboration Store project when I discovered a problem with the SNH Form Fields when running on my Tokyo instance. At the time, I was not able to diagnose the source of the problem, but I did manage to come up with a work-around, which I implemented on the page that I was developing at the time. What I did not do was to go back and refactor all of the other widgets that utilize the snh-form-field tag to implement the work-around on those as well, nor did I invest any time in actually hunting down the source of the actual problem with the tag, correcting it, and producing a new version.
Recently, I was working on my little Service Account Management app, and was rudely reminded of this unfortunate oversight. Initially, I thought that there was something wrong with my modal pop-up box, but after further review I realized this was the same snh-form-field issue that I had run into earlier on the other project. Clearly, it was long since time to address it.
To implement the work-around, I brought up a list of all of the Service Portalwidgets that contained the text ‘snh-form-field’ in the Body HTML template property. Then one by one, I pulled them up in the editor, searched for the tag, and then wrapped a SPAN around each one, mitigating the problem. For example, here is the original HTML for the Aggregate Column Editor widget:
It was not difficult work, but it was rather tedious. Eventually, I got through the entire list. Then I put together a new Update Set for the SNH Data Table Widgets and posted the new version (2.4) out on Share. Unfortunately, it wasn’t until I had already posted it out there that I realized that I had left out a critical widget in the build, so I had to build the Update Set a second time. It did not look like there was any way to replace the Update Set on Share for the 2.4 version, so I called the corrected Update Set 2.4.1. But that is not a legal version name on that site, so on Share, that version is known as 2.41. Anyway, it’s out there now, so if you are running, or planning to run, on Tokyo or Utah, you should definitely go out to Share and pull down the latest Update Set. But stay away from version 2.4, because that was just an error, and shouldn’t even be out there.
Oh, and if you run into any issues with the 2.4.1 version, please provide some details in the discussion section on Share, or in the comments below. Thanks!
“Walk that walk and go forward all the time. Don’t just talk that talk, walk it and go forward. Also, the walk didn’t have to be long strides; baby steps counted too. Go forward.” — Chris Gardner
Last time, we added some code to our storefront to launch the application installation process. Today, we want to build a detail screen to show all of the detailed information for a specific app. Although we could create a new UI Page or Portal Page for that, and have the tile click branch to that page, it seems as if it would be better if we just had a simple modal pop-up screen so that we could remain on the main shopping experience page after closing the modal dialog. To begin, let’s just create a simple Service Portalwidget that we can call up using spModal. We can call our widget Application Details and give it an ID of cs-application-details.
We have already gathered up the application data in the main widget, so we could simply pass everything that we have over to the pop-up widget; however, that would make the pop-up widget highly dependent on the main widget, which is something that I try to avoid. I think the better approach will be to pass in the sys_id of the app and let the pop-up widget fetch its own data from the database. For that, we can cut and paste most of the code that we already have in the main widget.
(function() {
if (input) {
data.sysId = input.sys_id;
data.record = {};
var appGR = new GlideRecord('x_11556_col_store_member_application');
appGR.query();
if (appGR.get(data.sysId)) {
var item = {};
data.record.name = appGR.getDisplayValue('name');
data.record.description = appGR.getDisplayValue('description');
data.record.logo = appGR.getValue('logo');
data.record.version = appGR.getDisplayValue('current_version');
data.record.provider = appGR.getDisplayValue('provider.name');
data.record.providerLogo = appGR.provider.getRefRecord().getValue('logo');
data.record.local = appGR.getDisplayValue('provider.instance') == gs.getProperty('instance_name');
data.record.state = 0;
if (appGR.getValue('application')) {
data.record.state = 1;
data.record.installedVersion = appGR.getDisplayValue('application.version');
if (data.record.version == data.record.installedVersion) {
data.record.state = 2;
}
}
if (!data.record.local && data.record.state != 2) {
data.record.attachmentId = getAttachmentId(data.record.sys_id, data.record.version);
}
}
}
function getAttachmentId(applicationId, version) {
var attachmentId = '';
var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
versionGR.addQuery('member_application', applicationId);
versionGR.addQuery('version', version);
versionGR.query();
if (versionGR.next()) {
var attachmentGR = new GlideRecord('sys_attachment');
attachmentGR.addQuery('table_name', 'x_11556_col_store_member_application_version');
attachmentGR.addQuery('table_sys_id', versionGR.getUniqueValue());
attachmentGR.addQuery('content_type', 'CONTAINS', 'xml');
attachmentGR.query();
if (attachmentGR.next()) {
attachmentId = attachmentGR.getUniqueValue();
}
}
return attachmentId;
}
})();
Before we get too excited about formatting all of this data for display, let’s just throw a single value onto the display and see if we can get the mechanics of bringing up the widget all working. Here is some simple HTML to get things started.
<div>
<h3>{{c.data.record.name}}</h3>
</div>
That should be enough to get things going, so let’s pop back over to the main widget and see if we can set up a function in the Client script to call up this widget.
That should be enough to be able to open up the store and give things the old college try.
Not bad. OK, now that we have the basic mechanics working, we need to design the layout of the pop-up and also add whatever functionality we might want such as links to any forms or actions. At this point in the process, we have not added that much in the way of extra data. There are no categories or keywords or comments or user ratings or statistics or much of anything else in the way of interesting information outside of the name and description of the application. Some or all of that may come in some future version, but for now, about the best we can do to add detail would be to add the version history and to throw in a few useful links.
The above example is for an app pulled down from the store. For local apps that have been pushed up to the store, the look would be similar, but with a few differences.
For the local application, we should probably continue with the modified background, just to be consistent, but you get the idea. To pull this off, we will have to fetch more data from the database, and work out all of the associated HTML. Once that is done, that should be good enough to push out a new version so that folks can try it all out at home. That’s still a bit of work, so let’s deal with all of that next time out.
“Well, if it can be thought, it can be done, a problem can be overcome.” — E.A. Bucchianeri
The snh-form-field tag has been around for a while in various incarnations, and has been used on quite a few projects for one thing or another, all with relative success. Several snh-form-fields make up the initial set-up screen for the Collaboration Store, which also worked out quite well for that particular effort. At least, it was working out quite well until I tried to set up a new instance that was running the new Tokyo release. Here is what the initial set-up screen is supposed to look like:
Here is what it looks like on a Tokyo instance:
As you can clearly see, a couple of very important input fields are not on the screen in the Tokyo version. This appears to be due to a flaw in the snh-form-field tag that did not reveal itself in the earlier versions of the Now Platform. At this point, I do not know the exact nature of the flaw, only that it appears that you can only have one snh-form-field tag in any given container. I would categorize this as a flaw in the tag rather than an issue with the new release of ServiceNow, mainly because of the first rule of programming. It may take me a while to figure out exactly what might be going on here, but in the interim, I was at least able to come up with a viable work around. If you are using the snh-form-field tag on a project running on a Tokyo instance and you run into this issue before the flaw in the tag has been corrected, you can simply surround each snh-form-field tag with a span. That places each snh-form-field tag in its own personal container, eliminating the issue.
Here is how I used this technique to work around the problem that I was having with the initial set-up screen for the Collaboration Store.
<div id="nav_message" class="outputmsg_nav" ng-hide="c.data.validScope">
<img role="presentation" src="images/icon_nav_info.png">
<span class="outputmsg_nav_inner">
The <strong>Collaboration Store Set-up</strong> cannot be completed because <strong>Collaboration Store</strong> is not selected in your application picker.
<a onclick="window.location.href='change_current_app.do?app_id=5b9c19242f6030104425fcecf699b6ec&referrer=%24sp.do%3Fid%3Dcollaboration_store_setup'">
Switch to <strong>Collaboration Store</strong>
</a>.
</span>
</div>
<div class="row" ng-show="c.data.phase != 1 && c.data.phase != 2 && c.data.phase != 3">
<div class="col-sm-12">
<h4>
<i class="fa fa-spinner fa-spin"></i>
${Wait for it ...}
</h4>
</div>
</div>
<div class="row" ng-show="c.data.phase == 1">
<div style="text-align: center;">
<h3>${Collaboration Store Set-up}</h3>
</div>
<div>
<p>
Welcome to the Collaboration Store set-up process.
There are two ways that you can set up the Collaboration Store on your instance:
1) you can be the Host Instance to which all other instances connect, or
2) you can connect to an existing Collaboration Store with their permission.
To become the Host Instance of your own Collaboration Store, select <em>This instance will be
the Host of the store</em> from the Installation Type choices below.
If you are not the Host Instance, then you will need to provide the Instance ID of the
Collaboration Store to which you would like to connect and the Host instance will need to be
available to complete the set-up process.
</p>
</div>
<form id="form1" name="form1" novalidate>
<div class="row">
<div class="col-xs-12 col-sm-6">
<span>
<snh-form-field
snh-model="c.data.instance_type"
snh-name="instance_type"
snh-label="${Installation Type}"
snh-type="select"
snh-required="true"
snh-choices='[{"value":"host", "label":"This instance will be the Host of the store"},
{"value":"client", "label":"This instance will connect to an existing store"}]'/>
</span>
<span>
<snh-form-field
snh-model="c.data.host_instance_id"
snh-name="host_instance_id"
snh-label="${Host Instance ID}"
snh-help="Enter the instance ID only, not the full URL of the instance (https://{instance_id}.servicenow.com))"
snh-required="c.data.instance_type == 'client'"
ng-show="c.data.instance_type == 'client'"/>
</span>
<span>
<snh-form-field
snh-model="c.data.store_name"
snh-name="store_name"
snh-label="${Store Name}"
snh-required="c.data.instance_type == 'host'"
ng-show="c.data.instance_type == 'host'"/>
</span>
<span>
<snh-form-field
snh-model="c.data.instance_name"
snh-name="instance_name"
snh-label="${Instance Label}"
snh-required="true"/>
</span>
</div>
<div class="col-xs-12 col-sm-6 text-center">
<div class="row" style="padding: 15px;">
<div class="avatar-extra-large avatar-container" style="cursor:default;">
<div class="avatar soloAvatar bottom">
<div class="sub-avatar mia" ng-style="instanceLogoImage"><i class="fa fa-image"></i></div>
</div>
</div>
</div>
<div class="row" style="padding: 15px;">
<input ng-show="false" type="file" accept="image/jpeg,image/png,image/bmp,image/x-windows-bmp,image/gif,image/x-icon,image/svg+xml" ng-file-select="attachFiles({files: $files})" />
<button ng-click="uploadInstanceLogoImage($event)"
ng-keypress="uploadInstanceLogoImage($event)" type="button"
class="btn btn-default send-message">${Upload Instance Logo Image}</button>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<span>
<snh-form-field
snh-model="c.data.email"
snh-name="email"
snh-label="${Email}"
snh-help="A verification email will be sent to this address as part of the set-up process"
snh-type="email"
snh-required="true"/>
</span>
<span>
<snh-form-field
snh-model="c.data.description"
snh-name="description"
snh-label="${Instance Description}"
snh-type="textarea"
snh-required="true"/>
</span>
</div>
</div>
</form>
<div class="row">
<div class="col-sm-12" style="text-align: center;">
<button class="btn btn-primary" ng-disabled="!(form1.$valid) || !c.data.validScope" ng-show="c.data.instance_type == 'host'" ng-click="save();">${Create New Collaboration Store}</button>
<button class="btn btn-primary" ng-disabled="!(form1.$valid) || !c.data.validScope" ng-show="c.data.instance_type == 'client'" ng-click="save();">${Complete Set-up and Request Access}</button>
</div>
</div>
</div>
<div class="row" ng-show="c.data.phase == 2">
<div style="text-align: center;">
<h3>${Email Verification}</h3>
</div>
<div>
<p>
A verification email has been sent to {{c.data.email}} with a one-time security code.
Please enter the code below to continue.
</p>
<p>
Cancelling this process will terminate the set-up process.
</p>
</div>
<form id="form2" name="form2" novalidate>
<div class="row">
<div class="col-sm-12">
<span>
<snh-form-field
snh-model="c.data.security_code"
snh-name="security_code"
snh-label="Security Code"
snh-required="true"
placeholder="Enter the security code sent to you via email"/>
</span>
</div>
</div>
</form>
<div class="row">
<div class="col-sm-12" style="text-align: center;">
<button class="btn btn-default" ng-disabled="!(form2.$valid)" ng-click="cancel();">${Cancel}</button>
<button class="btn btn-primary" ng-disabled="!(form2.$valid)" ng-click="verify();">${Submit Verification Code}</button>
</div>
</div>
</div>
<div class="row" ng-show="c.data.phase == 3">
<div style="text-align: center;">
<h3>${Set Up Complete!}</h3>
</div>
<div>
<p>${Congratulations!}</p>
<p ng-if="c.data.instance_type == 'client'">
The Collaboration Store set-up is now complete. Your instance has been successfully registered with the
<b class="text-primary">{{c.data.registeredHostName}}</b> ({{c.data.registeredHost}})
Host and is now ready to utilize the Collaboration Store features.
</p>
<p ng-if="c.data.instance_type == 'host'">
The Collaboration Store set-up is now complete. Your instance has been successfully set up as a
Host instance and is now ready to accept client registrations and utilize the Collaboration
Store features.
</p>
</div>
</div>
It’s not the best, but it does work. If you are one of those kind souls helping out with the testing of the Collaboration Store, and you happen to be doing your testing on a Tokyo instance, you can just grab the code above and overlay the HTML for the set-up widget with this version. That should get you up and running again. This will definitely be included in the next early release for testing purposes, but if you don’t want to wait for that, just snag the code above.
The best solution, of course, is to continue digging until we find the source of the true problem here and release a new version of SNH Form Fields that does not contain this issue. Hopefully, it won’t be too long before we can make that happen.
“Set your goals high, and don’t stop till you get there.” — Bo Jackson
Last time, we started hacking up a copy of the Service Portalpagesc_category, beginning with the HTML. Now we need to update the Server script to pull in our data so that we can bring up the widget and see how it looks. For the header portion, we need to pull in data about the store, which we can get from the instance record for the Host instance.
function fetchStoreDetails() {
var instanceGR = new GlideRecord('x_11556_col_store_member_organization');
if (instanceGR.get('instance', gs.getProperty('x_11556_col_store.host_instance'))) {
data.store = {};
data.store.name = instanceGR.getDisplayValue('name');
data.store.description = instanceGR.getDisplayValue('description');
data.store.logo = instanceGR.getValue('logo');
data.store.sys_id = instanceGR.getUniqueValue();
}
}
For the apps, we look to the application table. At this point, we just want to pull in all of the data, so we will save any filtering for later in the development process. Right now, we just want to see how our presentation is coming out.
Some of that is still left over from the original widget, but we’ll clean that up later. For now, we just want to get to the point where we can throw this widget on a page and bring it up and see what we have. A lot of the other code from the original can be tossed, but we will need to retain a few things related to the paging, which leaves us with this.
That should be enough to make it work. Now let’s create a page for our widget, call it collaboration_store, and bring it up in the Service Portal Designer. To begin, let’s drag a 3/9 container onto the page.
Once we have our container in place, let’s find our new widget and drag it into the 9 portion of the 3/9 container.
Eventually, we will want to come up some kind of search/filter widget for the 3 section of the 3/9 container, but for now we will just leave that empty, save what we have, and pull up the page in the Service Portal to see how things are looking.
Not bad! Of course, we still have a lot of work to do on the Client script to handle things like switching to the alternate view, clicking on a tile, or paging through lengthy results, but the look and feel seems to work so far. I think it might be good to put the logo of the store in the header, but other than that, I do not see any significant changes that I would like to make at this point. I think it actually looks pretty good.
We’ll need to start working on the Client script now, once again stripping out those things related to catalogs and categories, and making sure that the toggle functions still work for switching back and forth between the tiles layout and the table layout. Also, we will need to figure out what we want to do when the operator clicks on an app. We could bring up the details of the application in a modal pop-up, navigate to a detail page, or even go straight into launching an install. That will all end up in the Client script as well.
One thing that we will need to add to the view is the state of the app on the instance, whether it is up to date, needs to be upgraded to the latest version, or has never been installed. The state of the app may determine what happens when you click on the app, so we will need to figure all that out as well. So much to figure out, but still it is a pretty good start. Next time, we will just keep forging ahead unless we have some test results to address.
“Never look down to test the ground before taking your next step; only he who keeps his eye fixed on the far horizon will find the right road.” — Dag Hammarskjold
Last time, we took a look at a number of different examples of pages that could serve as a model for locating an app in the Collaboration Store. We decided to use the Service Portalpagesc_category, the Service Catalog category browse page, as a starting point for our efforts. Taking a look at the page, we can see two containers containing three rows of various widgets.
The only thing that we really need off of this page at this point is our own copy of the SC Category Page widget, so let’s go make a copy of that and call it Collaboration Store, since this is basically going to be the storefront of our Collaboration Store.
Once we make the copy, we can update the Name, ID, and Description fields and save our new widget.
Now that we have our own copy to play with, let’s take a look at that HTML and see what we want to keep and what we want to toss.
Starting at the top, the All Categories button has no use in our scenario, so we can cut that part out. The category title and category description data can be replaced with the name and description of the store (the Host instance). The next block contains the pair of icons used to select between a display of tiles or a simple list. I like those options, so we will keep that section intact. Following that we have a loading block and a nothing to see here block, both of which would seem to have a valid use in our adaptation, so we will leave those there for now as well.
The next section is the table view, with columns for Item, Description, and Price. We will do something similar, but our columns will be Application, Description, Version, and Provider. The next section is the tile view, and we will work our same data points into the tile layout as well. The final block is all of the elements of the optional Show More Items section, and we can just leave that in place for now. That leaves our HTML looking something like this:
Now that we have the HTML all roughed out, it would nice to bring it up and see how it looks, but we are going to need some data first. For that, we are going to have take a look at the widget’s server-side script. Let’s take a quick peek and see what it is that we have to work with.
(function() {
if (input && input.category_id)
data.category_id = input.category_id;
else
data.category_id = $sp.getParameter("sys_id");
data.catalog_id = $sp.getParameter("catalog_id") ? $sp.getParameter("catalog_id") + "" : "-1";
var catalogsInPortal = ($sp.getCatalogs().value + "").split(",");
var isCatalogAccessibleViaPortal = data.catalog_id == -1 ? true : false;
catalogsInPortal.forEach(function(catalogSysId) {
if (data.catalog_id == catalogSysId) {
isCatalogAccessibleViaPortal = true;
}
});
data.categorySelected = gs.getMessage('category selected');
if(!isCatalogAccessibleViaPortal) {
data.error = gs.getMessage("You do not have permission to see this catalog");
return;
}
var catalogDisplayValue;
if (data.catalog_id && data.catalog_id !== "-1") {
var catalogObj = new sn_sc.Catalog(data.catalog_id);
if (catalogObj) {
if (!catalogObj.canView()) {
data.error = gs.getMessage("You do not have permission to see this catalog");
return;
}
catalogDisplayValue = catalogObj.getTitle();
}
}
if (options && options.sys_id)
data.category_id = options.sys_id;
data.showPrices = $sp.showCatalogPrices();
data.sc_catalog_page = $sp.getDisplayValue("sc_catalog_page") || "sc_home";
data.sc_category_page = $sp.getDisplayValue("sc_category_page") || "sc_category";
catalogDisplayValue = catalogDisplayValue ? catalogDisplayValue : $sp.getCatalogs().displayValue + "";
var catalogIDs = (data.catalog_id && data.catalog_id !== "-1") ? data.catalog_id : $sp.getCatalogs().value + "";
var catalogArr = catalogDisplayValue.split(",");
var catalogIDArr = catalogIDs.split(",");
data.sc_catalog = catalogArr.length > 1 ? "" : catalogArr[0];
data.show_more = false;
if (GlideStringUtil.nil(data.category_id)) {
data.items = getPopularItems();
data.show_popular_item = true;
data.all_catalog_msg = (($sp.getCatalogs().value + "").split(",")).length > 1 ? gs.getMessage("All Catalogs") : "";
data.all_cat_msg = gs.getMessage("All Categories");
data.category = {title: gs.getMessage("Popular Items"),
description: ''};
return;
}
data.show_popular_item = false;
// Does user have permission to see this category?
var categoryId = '' + data.category_id;
var categoryJS = new sn_sc.CatCategory(categoryId);
if (!categoryJS.canView()) {
data.error = gs.getMessage("You do not have permission to see this category");
return;
}
data.category = {title: categoryJS.getTitle(),
description: categoryJS.getDescription()};
var catalog = $sp.getCatalogs().value;
data.items = [];
var itemsInPage = options.limit_item || 9;
data.limit = itemsInPage;
if (input && input.new_limit)
data.limit = input.new_limit;
if (input && input.items) {
data.items = input.items.slice();//Copy the input array
}
if (input && input.startWindow) {
data.endWindow = input.endWindow;
}
else {
data.startWindow = 0;
data.endWindow = 0;
}
while (data.items.length < data.limit + 1) {
data.startWindow = data.endWindow;
data.endWindow = data.endWindow + itemsInPage;
var itemGR = queryItems(catalog, categoryId, data.startWindow, data.endWindow);
if (!itemGR.hasNext())
break;
fetchItemDetails(itemGR, data.items);
}
if (data.items.length > data.limit)
data.show_more = true;
data.more_msg = gs.getMessage(" Showing {0} items", data.limit);
data.categories = [];
while(categoryJS && categoryJS.getParent()) {
var parentId = categoryJS.getParent();
categoryJS = new sn_sc.CatCategory(parentId);
var category = {
label: categoryJS.getTitle(),
url: '?id='+data.sc_category_page+'&sys_id=' + parentId
};
data.categories.unshift(category);
}
data.all_catalog_msg = (($sp.getCatalogs().value + "").split(",")).length > 1 ? gs.getMessage("All Catalogs") : "";
function fetchItemDetails(itemRecord, items) {
while (itemRecord.next()) {
var catalogItemJS = new sn_sc.CatItem(itemRecord.getUniqueValue());
if (!catalogItemJS.canView())
continue;
var catItemDetails = catalogItemJS.getItemSummary();
var item = {};
item.name = catItemDetails.name;
item.short_description = catItemDetails.short_description;
item.picture = catItemDetails.picture;
item.price = catItemDetails.price;
item.sys_id = catItemDetails.sys_id;
item.hasPrice = catItemDetails.show_price;
item.page = 'sc_cat_item';
item.type = catItemDetails.type;
item.order = catItemDetails.order;
item.sys_class_name = catItemDetails.sys_class_name;
item.titleTag = catItemDetails.name;
if (item.type == 'order_guide') {
item.page = 'sc_cat_item_guide';
} else if (item.type == 'content_item') {
item.content_type = catItemDetails.content_type;
item.url = catItemDetails.url;
if (item.content_type == 'kb') {
item.kb_article = catItemDetails.kb_article;
item.page = 'kb_article';
} else if (item.content_type == 'external') {
item.target = '_blank';
item.titleTag = catItemDetails.name + " âžš";
}
}
items.push(item);
}
}
function queryItems(catalog, categoryId, startWindow, endWindow) {
var scRecord = new sn_sc.CatalogSearch().search(catalog, categoryId, '', false, options.show_items_from_child != 'true');
scRecord.addQuery('sys_class_name', 'NOT IN', 'sc_cat_item_wizard');
scRecord.addEncodedQuery('hide_sp=false^ORhide_spISEMPTY^visible_standalone=true');
scRecord.chooseWindow(startWindow, endWindow);
scRecord.orderBy('order');
scRecord.orderBy('name');
scRecord.query();
return scRecord;
}
function getPopularItems() {
return new SCPopularItems().useOptimisedQuery(gs.getProperty('glide.sc.portal.popular_items.optimize', true) + '' == 'true')
.baseQuery(options.popular_items_created + '')
.allowedItems(getAllowedCatalogItems())
.visibleStandalone(true)
.visibleServicePortal(true)
.itemsLimit(6)
.restrictedItemTypes('sc_cat_item_guide,sc_cat_item_wizard,sc_cat_item_content,sc_cat_item_producer'.split(','))
.itemValidator(function(item, itemDetails) {
if (!item.canView() || !item.isVisibleServicePortal())
return false;
return true;
})
.responseObjectFormatter(function(item, itemType, itemCount) {
return {
order: 0 - itemCount,
name: item.name,
short_description: item.short_description,
picture: item.picture,
price: item.price,
sys_id: item.sys_id,
hasPrice: item.price != 0,
page: itemType == 'sc_cat_item_guide' ? 'sc_cat_item_guide' : 'sc_cat_item'
};
})
.generate();
}
function getAllowedCatalogItems () {
var allowedItems = [];
catalogIDArr.forEach(function(catalogID) {
var catalogObj = new sn_sc.Catalog(catalogID);
var catItemIds = catalogObj.getCatalogItemIds();
for(var i=0; i<catItemIds.length; i++) {
if (!allowedItems.includes(catItemIds[i]))
allowedItems.push(catItemIds[i]);
}
});
return allowedItems;
}
})();
There is a lot here to digest, and an awful lot that is not relevant to our purpose, particularly all of those things that are related to catalogs and categories. We may want to just toss this out and replace it with some simple logic to pull in the Host information for the header and then all of the apps for the main section. Either way, this seems like a little more work than just rearranging the HTML, so let’s save all of that for our next installment.
“Imagination is the beginning of creation. You imagine what you desire, you will what you imagine and at last you create what you will.” — George Bernard Shaw
While we wait patiently for some comments from the folks gracious enough to do some testing with the latest release, let’s take a quick peek ahead something that we might want to tackle next. We are rapidly winding down the initial goal of getting all of the basic mechanics working for installing, setting up, and using the app to move artifacts between instances. Once we have all of that working satisfactorily, we will want to expand our efforts to include some of those other items on our list of things that we would still like to do. One of the major items on that list is the shopping experience, or the way in which a developer would locate an app in the store. Just focusing on the presentation itself for just a moment, let’s take a look at some of the many similar experiences for that there are right now.
All Applications
If you select plugins on the primary navigation, you get redirected to the All Applications page.
This page features a left-hand panel for various filter options and then a right-hand panel of full-width application tiles that include details about the app and two installation options, scheduled and immediate. In the upper right-hand corner there is a Find in Store button, which will take you to our next example.
App Store
This one also has a left-hand filter column and a right-hand column of application tiles, but on this one the tiles are not full-width and there are no install options. There is a cool mouse-over feature, though, and this one includes user-ratings, which can also be used as filter and sort options.
My Company Applications
The application manager built into the Now Platform uses tabs rather than filters, and the application tiles are full width, but do not include the description of the app. Rather than install options there is an Edit button, which makes it look similar in appearance to the first example.
Service Catalog
Not an application shopping experience, but a shopping experience just the same, and with many characteristics similar to those of the other examples.
This one has a left-hand panel for selecting a category from a category tree and the tiles in the right-hand panel are more like those in the App Store. Obviously, the content of the tiles would have to be adjusted to include the relevant data for applications, but the one benefit of this one over the others is that we have access to the source code. This is a standard Service Portal Page, which we could potentially clone and then modify for our purpose. The same is also true of the individual widgets that make up the page, as they could also be either cloned or replaced.
At this stage of the process, we do not have many of the features that make up much of the content in some of these examples. Today, there are no categories, no tags, no reviews, no ratings, and no other helpful ways to filter the list down to the things in which you might have an interest. Introducing any such features would be a project in and of itself, but we can still attempt to build a rudimentary shopping experience without those, and then throw those in later as the needs arise. For now, there aren’t that many test apps in the working model, so filtering is not yet a pressing need.
Just to see what we can do, let’s grab a copy of the catalog portal page next time and start hacking it up to meet our needs. It won’t necessarily be everything that it could be right at the start, but it will be a beginning.
“Unplanned occurrences are reminders to check your tendency to think that you’re the one in control.” — James Martin
Last time, we created another example of how one might utilize the new scripted value column feature, this time with catalog item variables instead of Incident journal entries. There are a number of other things that we could try, but two examples should be enough to get the point across, and I’ll leave it to others to come up with additional examples of their own.
We still have two more wrapper widgets to update, though, and we still have that annoying misalignment between the original columns and the new. Here is the way things come out right now:
… and here is the way that it should look:
I was able to capture that second image because I found and fixed the problem. I had to replace this HTML:
Now, without getting into too much detail that no one really cares about, the source of the problem was the sn-avatar tag, which I added a while back so that user columns would have the avatar in front of the name. For some reason, the tag renders out a carriage return and a handful of spaces just before the avatar image. With the ng-if attribute set to false, this collection of white space is still rendered on the page, even when the avatar itself is not. I solved that problem by wrapping the avatar tag with a span and putting the ng-if attribute on the outer span rather than on the sn-avatar tag. That took care of things for columns where there was no avatar, but the user columns, which show the avatar, were still out of alignment with the rest of the columns. Adding style=”display: inline-flex;“ took care of that problem with the avatar, but then the user name ended up underneath the avatar instead of next to it. To solve that problem, I wrapped the whole thing in another span with the same style attribute. Now everything lines up the way that it should.
Now that that is out of the way, we still have two more wrapper widgets to update. Let’s jump into the SNH Data Table from Instance Definition and do the same kind of searching we did before, looking for some code that might need to be copied and modified. On this particular widget, such a search turns up nothing at all in either the Server script or the Client script, so the only thing that we really need to do is to add another entry to the Option schema for our new scripted value column specification.
{"hint":"A JSON object containing the specifications for scripted value columns",
"name":"scripteds",
"default_value":"",
"section":"Behavior",
"label":"Scripted Value Column Specifications (JSON)",
"type":"String"}
To test this, we can modify our scripted_value_test_2 page to use this widget instead of the SNH Data Table from JSON Configuration widget, and then transfer our configuration options from the Script Include to the widget options.
Now all we need to do is to save it and then run out to the Service Portal and take a quick peek.
So that all looks good. And much, much better now that the column data all lines up as it should! It’s nice to finally have that fixed. That takes care of wrapper widget #2. Now let’s take a look at that last one, the SNH Data Table from URL Definition widget. The only line that appears to require modification is this one:
Now we need to test it, so we will need to find or create a page that use this widget. Let’s take a look at the ones that are already out there by checking out the Related List down at the bottom of the form.
The page my_things looks like a good candidate, so we can take a look at the configuration script that it uses and then edit it to add one or more scripted value columns. One of the tables utilized on that page is the Incident table, so let’s go ahead and use our existing value provider script to add a journal entry column to one of those.
Well, that didn’t work! It’s always something. Even if there were no comments on any of these Incidents, we should still have a column heading for our new scripted value column. I don’t think this problem is in the widget that we just modified, however. This widget shares the page with the Configurable Data Table Widget Content Selector widget, and that is a widget that we have not even touched. That is going to have to be modified to accommodate our new feature as well, as it builds the URL that the SNH Data Table from URL Definition widget turns to for its configuration information. This was not on our list of things to do for this feature, but it definitely needs to be done.
I was hoping to wrap things up with this installment, but now we have a new widget to modify and more testing to do, so I think we will just save all of that, plus the Update Set creation, for our next time out.
“Do not be too timid and squeamish about your actions. All life is an experiment. The more experiments you make the better.” — Ralph Waldo Emerson
Last time, we used the new scripted value columns feature to create columns from the comments and work notes of an Incident. Today we are going to do something similar, but instead of working with journal entries, we will try something with the optional variables that can be associated with Service Catalog items. For our demonstration, we will use the Executive Desktop catalog item, which has a number of defined ordering options.
Let’s see if we can’t create a list of all orders for this item, and include some of the catalog item variables on the list. To begin, let’s make another copy of our example scripted value provider and call it ScriptedCatalogValueProvider.
Let’s also make a copy of our last configuration script and change things over from the Incident table to the Requested Item table, and define some scripted value columns for some of the catalog item variables associated with this item.
Finally, let’s make a copy of our last test page and call it scripted_value_test_2, and then edit the widget options to use our new configuration file. Now let’s jump out to the Service Portal and pull up the page and see what we have so far.
So far, so good. Everything seems to work, but of course all we have in our new columns is the random numbers that we threw in there for demonstration purposes earlier. But that just means that all we have left to do now is to figure out what kind of script we need to pull out the actual variable values that are associated with each catalog item order. To begin, let’s map our variable names, which are also our column names, to their catalog item variable (question) sys_ids.
This will allow us to use some common code for all of the columns by passing in the correct sys_id for the catalog item variable associated with that column.
getScriptedValue: function(item, config) {
var response = '';
var column = config.name;
if (this.questionMap[column]) {
response = this.getVariableValue(this.questionMap[column], item.sys_id);
}
return response;
}
Now we need to build the getVariableValue function, which takes the sys_id of the question and the sys_id of the requested item as arguments. To locate the value selected for this question for this order, we use the sc_item_option_mtom table, which maps requested items to individual question responses.
getVariableValue: function(questionId, itemId) {
var response = '';
var mtomGR = new GlideRecord('sc_item_option_mtom');
mtomGR.addQuery('request_item', itemId);
mtomGR.addQuery('sc_item_option.item_option_new', questionId);
mtomGR.query();
if (mtomGR.next()) {
response = mtomGR.getDisplayValue('sc_item_option.value');
}
return response;
}
Now let’s save that and give the page another look.
Well, that’s better, but it is still not exactly what we want. The values that appear in the columns are the raw values for the variables, but what we would really like to see is the display value. To get that, we have to go all the way back to the original question choices and find the choice record that matches both the question and the answer. For that, you need the sys_id of the question and the value of the answer.
getDisplayValue: function(questionId, value) {
var response = '';
var choiceGR = new GlideRecord('question_choice');
choiceGR.addQuery('question', questionId);
choiceGR.addQuery('value', value);
choiceGR.query();
if (choiceGR.next()) {
response = choiceGR.getDisplayValue('text');
}
return response;
}
To utilize this new function, we have to tweak our getVariableValue function just a little bit to make the call.
getVariableValue: function(questionId, itemId) {
var response = '';
var mtomGR = new GlideRecord('sc_item_option_mtom');
mtomGR.addQuery('request_item', itemId);
mtomGR.addQuery('sc_item_option.item_option_new', questionId);
mtomGR.query();
if (mtomGR.next()) {
var value = mtomGR.getDisplayValue('sc_item_option.value');
if (value) {
response = this.getDisplayValue(questionId, value);
}
}
return response;
}
Now let’s take one more look at that page now that we have made these modifications.
That’s better! Now we have the same text in our columns that the user saw when placing the order, and that’s what we really want to see here. Or at least, that’s what I wanted to see. You may be interested in something completely different, but that’s the whole point of this approach. You can basically script whatever you want to put whatever you want to put in whatever column or columns you would like to define. These are just a couple of examples, but there really is no limit to what you might be able to do with a little imagination and some Javascript.
So here is the final version of our second close-to-real-world example value provider script:
var ScriptedCatalogValueProvider = Class.create();
ScriptedCatalogValueProvider.prototype = {
initialize: function() {
},
questionMap: {
cpu: 'e46305fbc0a8010a01f7d51642fd6737',
memory: 'e463064ac0a8010a01f7d516207cd5ab',
drive: 'e4630669c0a8010a01f7d51690673603',
os: 'e4630688c0a8010a01f7d516f68c1504'
},
getScriptedValue: function(item, config) {
var response = '';
var column = config.name;
if (this.questionMap[column]) {
response = this.getVariableValue(this.questionMap[column], item.sys_id);
}
return response;
},
getVariableValue: function(questionId, itemId) {
var response = '';
var mtomGR = new GlideRecord('sc_item_option_mtom');
mtomGR.addQuery('request_item', itemId);
mtomGR.addQuery('sc_item_option.item_option_new', questionId);
mtomGR.query();
if (mtomGR.next()) {
var value = mtomGR.getDisplayValue('sc_item_option.value');
if (value) {
response = this.getDisplayValue(questionId, value);
}
}
return response;
},
getDisplayValue: function(questionId, value) {
var response = '';
var choiceGR = new GlideRecord('question_choice');
choiceGR.addQuery('question', questionId);
choiceGR.addQuery('value', value);
choiceGR.query();
if (choiceGR.next()) {
response = choiceGR.getDisplayValue('text');
}
return response;
},
type: 'ScriptedCatalogValueProvider'
};
The added columns still do not align with the original columns, which I would like to fix before I build a new Update Set, and we still have a couple more wrapper widgets to address, but I think we are getting close. Maybe we can wrap this whole thing up in our next installment.