“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!
“Everything ends; you just have to figure out a way to push to the finish line.” — Jesse Itzler
Last time, we wrapped up the work on the example Service Account dashboard, although we did leave off a few potential enhancements that could improve its value. There is always more that could be done, such as the addition of an Admin Perspective showing all of the accounts and requests or an Expiring State showing all of the accounts that are coming up for review. Since this is just an example, we don’t need to invest the time in building all of those ideas out; some things should be left as an exercise for those who would like to pull this down and play around with it.
What we should do now, though, is take a quick step back and see what we have so far and what might be left to do before we can call this good enough to push out. When we first set out to do this, we identified the following items that would need to be developed:
One or more Service Catalog items to create, alter, and terminate accounts
A generic workflow for the catalog item(s)
A type-specific workflow for each type of account in the type table
Some kind of periodic workflow to ensure that the account is still needed.
We have basically created everything on our list except for that last item, but we have also indicated that the process to check back every so often and see if the account was still needed is something that could be handled by a stand-alone generic product that could perform that function for all kinds of things that would benefit from a periodic review. If we assume that we will turn that process over to a third party, then we would seem to have just about everything that we need.
There is one other thing that would be helpful, though, and we neglected to included it on our original list. It would be nice to have some kind of menu item to launch all of these processes that we have built, so let’s put that together real quick and get that out of the way. I am thinking of something like this:
Service Accounts
New Service Account
My Service Accounts
Service Accounts
Service Account Types
The first item would initiate a request for the Service AccountCatalog Item, the second would bring up the dashboard, and the last two would just bring up the list view of our two tables. Those last two would also be limited to admins only and the rest would be open to everyone. Here is the high-level menu entry.
… and here are the four submenu options for this high-level menu item:
Which produces a menu that looks like this:
So that’s about it for this little example project. Again, this is not intended to be a fully functional product that you would simply install and start using. This is just an example with enough working parts to get things started for anyone who might want to try to create something along these lines. Obviously, you would have your own list of types, your own implementation workflows for each type, your own approval structure for each type, and your own language in all of the notices, so it’s not as if someone could build all of that out in a way that would work for everyone. But for anyone would like a set of parts to play with to get things started, here is an Update Set that contains everything that we have put together during this exercise.
“Baby steps count, as long as you are going forward. You add them all up, and one day you look back and you’ll be surprised at where you might get to.” — Chris Gardner
Version 2.5 is essentially the exact same bundle as the previous version (2.4.1), with the only change being the inclusion of the corrected configuration editor. Still, it does address the issues related to scoped configuration scripts, so it’s probably worth pulling down and installing it, just to avoid running into those annoying problems one day in the future. There are no new features or components in this new version, but it does now include the latest of everything, so this is the one that you will want.
“You’ve got to think about big things while you’re doing small things, so that all the small things go in the right direction.” — Alvin Toffler
Recently I was playing around with the Content Selector Configuration Editor to create a dashboard for my Service Account Management app, which is a Scoped Application, and realized that the last fix that I put in to make things work with a Scoped Application did not quite go far enough. Looking things over it is quite clear that the original design had only considered support for global scripts, and my first attempt to rectify that oversight did not resolve all of the issues for configuration scripts that were not in the global scope. Today it is time to finish that up and correct all of the other shortcomings in that tool when working outside of the global scope.
For starters, the pick list for available scripts in the current version includes all of the configuration scripts for all scopes. What we really need is to limit that selection list to just those scripts in the current scope. Otherwise, you could potentially be editing a script in one scope while you are working in another scope, which will not end well if it works at all. To limit the list to just the scripts in the current scope, we need to add something like this to the filter:
It would also be good to add a little more information to the help text for that field, so the entire snh-form-field tag now looks like this:
<snh-form-field
snh-label="Content Selector Configuration"
snh-model="c.data.script"
snh-name="script"
snh-type="reference"
snh-help="Select the Content Selector Configuration from the current Scope that you would like to edit."
snh-change="scriptSelected();"
placeholder="Choose a Content Selector Configuration"
table="'sys_script_include'"
default-query="'active=true^sys_scope=javascript:gs.getCurrentApplicationId()^scriptLIKEObject.extendsObject(ContentSelectorConfig^ORscriptLIKEObject.extendsObject(global.ContentSelectorConfig'"
display-field="'name'"
search-fields="'name'"
value-field="'api_name'"/>
That solves one problem, but there are others. When building the new script from the user’s input in the save() function of the widget’s server script, this conditional only reduces the API Name to the root name for global scripts:
if (data.scriptInclude.startsWith('global.')) {
data.scriptInclude = data.scriptInclude.split('.')[1];
}
This needs to be done for scripts in any scope, so the entire conditional should just go away and simply be reduced to this:
Further down in that same function, this line again assumes that you are working in the global scope:
scriptGR.api_name = 'global.' + name;
The API Name is actually set for you whenever you save a new script, so this line can actually just be removed entirely and things will work just fine.
With all of these changes, the new save() function now looks like this:
All in all, not a huge number of changes, but just enough to make things work. I bundled all of the relevant parts into another Update Set that includes these various changes, which you can find here. This component is also a part of the larger SNH Data Table Widget collection, so eventually I will need to publish a new version of that collection out on Share as well.
“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!
“Whenever you have taken up work in hand, you must see it to the finish. That is the ultimate secret of success. Never, never, never give up!” — Dada Vaswani
Last time, we built a companion widget to handle icons and bulk actions. Today we will do something a little different and build a companion widget to handle aggregate columns. Usually, clicking on a aggregate column will bring you to a list of the records represented by the value. If the value was 10, then you would expect to see a list of 10 records, either on a new page or in a modal pop-up. Linking to a new page is already built into the aggregate column specification, so our companion widget example will demonstrate the modal alternative. For our example, we will use catalog requests, with the aggregate column representing the number of requested items in each request.
Here is the configuration script used to produce this table:
Without a page_id property in the aggregate column specification, it will not automatically link to another page, but it will broadcast the click event, so we can use a version of the Simple List widget to display our records.
Clicking on an item should bring up the ticket page, where you can see more details about the item.
The reason we need to use a version of the Simple List widget is that the Simple List widget is configured using widget options, and the spModalopen() function does not provide the capability of setting the widget’s options. It does, however, provide the capability to configure widget input, so all we need to do is to add these few lines to the top of our version’s Server script to convert that input to options.
// begin mod
if (input && input.options) {
for (var name in input.options) {
options[name] = input.options[name];
}
}
// end mod
With that in place, we can now build a typical companion widget that listens for the aggregate click event and pops open our modal dialog using the modified Simple List widget.
And that’s all there is to that. That gives us three different examples covering buttons, icons, bulk actions, and now aggregate columns, so that should cover just about everything. Once you do a few and get the hang of how all of the parts play together, it’s pretty simple to create a new one for a different purpose. For those of you who like to play along at home, here is an Update Set that includes all of the parts and pieces for these various examples. Of course, you will need to install the SNH Data Table Widgets for any of this to be of any value, but you already knew that!
“If you set a good example you need not worry about setting rules.” — Lee Iacocca
Last time, we put together a simple companion widget for an SNH Data Table that contained a single button. Today we will try something a little more complicated, with three different buttons and three different bulk actions. For this example, we will create a list of records waiting for an approval decision from the perspective of the person who can make such a decision. All of our buttons and bulk actions will be related to passing judgement on the items listed.
Here is the configuration script that we will used to produce this table.
The buttons handle actions for individual items and the bulk actions perform the same functions, but for more than one item at a time. Clicking on a button that involves comments should bring up an input dialog.
Once the comments are entered, the companion widget should go ahead and update the database and inform the user of the action taken.
Once the list is refreshed to reflect the changes, we can demonstrate a similar process for bulk actions.
Once again, a dialog appears so that the rejection reason can be entered.
And once the comments have been entered, the action is taken for all selected records.
And once the action has been taken, the screen is again refreshed to reveal that there are no further items requiring approval decisions.
So that’s the concept. Now let’s take a look the companion widget that makes this all work. To begin, we throw in the same list of event names at the top like we do with all of the others.
We have to deal with both buttons and bulk actions for this one, but since they both do essentially the same thing, it would be nice to share as much of the code as possible. Since the only difference is that the buttons work on a single record and the bulk actions work on a list of records, we could convert the single record for the button into a list of one, and then hand off the work to a function that expected a list, which would then work for both. We still need two listeners, though, so let’s build those next.
$rootScope.$on(eventNames.buttonClick, function(e, parms) {
var action = parms.config.name;
var sysId = [];
sysId.push(parms.record.sys_id);
processAction(action, sysId);
});
$rootScope.$on(eventNames.bulkAction, function(e, parms) {
var action = parms.config.name;
var sysId = [];
for (var i=0; i<parms.record.length; i++) {
sysId.push(parms.record[i].sys_id);
}
processAction(action, sysId);
});
Our processAction function then will not know or care whether the action came from a button or a bulk action. Everything from this point on will be the same either way. Let’s take a quick peek at that function now.
The check of c.data.inProgress is just a defensive mechanism to ensure that we are only processing one action at a time. We set it to true when we start and to false when we are done, and if it is already set to true when we start, then we do nothing, as there is already another action in progress. The rest is just a check to see if we need to collect comments, and if not, we proceed directly to processing the action. If we do need to collect comments, then we call this function.
function getComments(action) {
var msg = 'Approval comments:';
if (action == 'reject') {
msg = 'Please enter the reason for rejection:';
}
spModal.prompt(msg, '').then(function(comments) {
c.data.comments = comments;
processDecision();
});
}
And in either case, we end up here to process the decision.
function processDecision() {
c.server.update().then(function(response) {
window.location.reload(true);
});
}
Basically, that just makes a call over to server side where the actual database updates take place. Here is the entire Client controller all put together.
api.controller = function($scope, $rootScope, $window, spModal) {
var c = this;
var eventNames = {
referenceClick: 'data_table.referenceClick',
aggregateClick: 'data_table.aggregateClick',
buttonClick: 'data_table.buttonClick',
bulkAction: 'data_table.bulkAction'
};
$rootScope.$on(eventNames.buttonClick, function(e, parms) {
var action = parms.config.name;
var sysId = [];
sysId.push(parms.record.sys_id);
processAction(action, sysId);
});
$rootScope.$on(eventNames.bulkAction, function(e, parms) {
var action = parms.config.name;
var sysId = [];
for (var i=0; i<parms.record.length; i++) {
sysId.push(parms.record[i].sys_id);
}
processAction(action, sysId);
});
function processAction(action, sysId) {
if (!c.data.inProgress) {
c.data.inProgress = true;
c.data.action = action;
c.data.sys_id = sysId;
c.data.comments = '';
if (action == 'reject' || action == 'approvecmt') {
getComments(action);
} else if (action == 'approve') {
processDecision();
}
c.data.inProgress = false;
}
}
function getComments(action) {
var msg = 'Approval comments:';
if (action == 'reject') {
msg = 'Please enter the reason for rejection:';
}
spModal.prompt(msg, '').then(function(comments) {
c.data.comments = comments;
processDecision();
});
}
function processDecision() {
c.server.update().then(function(response) {
window.location.reload(true);
});
}
};
The actual work of updating the approval records takes place over on the server side. Here is the complete Server script.
(function() {
if (input && input.sys_id && input.sys_id.length > 0) {
var total = 0;
var approvalGR = new GlideRecord('sysapproval_approver');
for (var i=0; i<input.sys_id.length; i++) {
approvalGR.get(input.sys_id[i]);
if (approvalGR.state == 'requested') {
approvalGR.state = 'approved';
if (input.action == 'reject') {
approvalGR.state = 'rejected';
}
var comments = 'Approval response from ' + gs.getUserDisplayName() + ':';
comments += '\n\nDecision: ' + approvalGR.getDisplayValue('state');
if (input.comments) {
comments += '\nReason: ' + input.comments;
}
approvalGR.comments = comments;
if (approvalGR.update()) {
total++;
}
}
}
if (total > 0) {
var message = total + ' items ';
if (total == 1) {
message = 'One item ';
}
if (input.action == 'reject') {
message += 'rejected.';
} else {
message += 'approved.';
}
gs.addInfoMessage(message);
}
}
})();
That’s all basic GlideRecord stuff, so there isn’t too much commentary to add beyond the actual code itself. And that’s all there is to that. I still want to bundle all of these example up into a little Update Set, but I think I will do one more example before we do that. We will take a look at that one next time out.
“Few things are harder to put up with than the annoyance of a good example.” — Mark Twain
A while back we pushed our SNH Data Table Widgets out to Share, and then later made a few revisions and enhancements to come up with the version that is posted out there now. After spending a little time refactoring and standardizing some of the processes, we now have a fairly consistent way of handling reference links, bulk actions, buttons, icons, and the new scripted value columns. In most cases, there is a $rootScope broadcast message that just needs to be picked up by a companion widget, and then the companion widget handles all of the custom stuff that won’t be found in the common components. Most of the work that we did during that development process was centered on the shared components; the few companion widgets that we did throw in as samples were not the focus of the discussion. Now that the development of the table widgets is behind us, it is a good time to spend a little more time on the companion widgets and how they augment the primary artifacts.
Let’s say that we have a policy that Level 2 and 3 technicians working Incidents should respond to all customer comments, and even without customer inquiries, no open ticket should ever go more than X number of days without some comment from the assigned technician indicating the current status. To assist the technician in managing that expectation, we could configure a list of assigned incidents that included some visual indication of the comment status of each.
For those tickets where a comment is needed, we would want to provide a means for the technician to provide that comment right from the list. One way to do that would be to pop up the Ticket Conversations widget in a modal dialog. This would allow the tech to both view the current conversion and add their own comments.
Once a comment has been provided, the comment appears in the conversation stream, after which additional comments can be provided or the pop-up window closed.
Once the modal pop-up window is closed, the list should be updated to reflect the new comment status of the incident.
Here is the configuration script to produce the above table using the SNH Data Table from JSON Configuration widget.
The Data Table widget can be configured to produce the list, but to launch the modal pop-up, you will need to add a companion widget to the page. A companion widget shares the page with a Data Table widget, but has no visual component. It’s job is simply to listen for the broadcast messages and take whatever action is desired when a message of interest is received. At the top of the Client script of all companion widgets, I like to throw in this little chunk of code to help identify the values used to distinguish each potential message.
In this particular case, we are looking for a button click, and since there are no other buttons configured in this instance, we don’t even need to check to see which button it was.
Basically, this is just your typical spModal widget open, passing in some widget input. In the case of the Ticket Conversations widget, you need both a table name and the sys_id of a record on that table. In our case, we know that the table is the Incident table, and we can obtain the sys_id of the incident in the row from the parameters passed in with the broadcast message. When the modal window is closed, there are two functions passed in as arguments to the then function, the first for a successful completion and the second for a cancellation. In our case, we want to reload the page for either result, so the logic for each is the same.
For most circumstances, this would be the extent of the widget. For the most part, companion widgets are pretty simple, narrowly focused components that are built for a single purpose: to accomplish something that is not built in to the standard artifacts. Everything else is left for the primary widgets. We couldn’t get away without throwing in a little bit of hackery, though, since that’s what we do around here, so in this particular example, we will need to add just a bit more to the widget before we can call it complete.
In our example, we are using the class of the button to visually identify the current state of the comments on each incident. This cannot be done with the standard button configuration, as the class name is one of the configuration properties, and it is applied to all buttons in the column. To produce specific class values for each row, we have to resort to using a scripted value column instead of configuring a button. Each scripted value column requires a script to produce the value, and for this particular example, our script looks like this:
var LastCommentValueProvider = Class.create();
LastCommentValueProvider.prototype = {
initialize: function() {
},
getScriptedValue: function(item, config) {
var className = '';
var helpText = '';
var journalGR = this.getLastJournalEntry(item.sys_id);
if (journalGR.isValidRecord()) {
if (journalGR.getValue('sys_created_by') == gs.getUserName()) {
if (journalGR.getValue('sys_created_on') > gs.daysAgo(7)) {
className = 'success';
helpText = 'Click here to add additional comments';
} else {
className = 'danger';
helpText = 'Click here to update the status';
}
} else {
className = 'danger';
helpText = 'Click here to respond to the latest comment';
}
} else {
className = 'default';
helpText = 'Click here to provide a status comment';
}
return this.getHTML(item, className, helpText);
},
getHTML: function(item, className, helpText) {
var response = '<a href="javascript:void(0)" role="button" class="btn-ref btn btn-';
response += className;
response += '" onClick="tempFunction(this, \'';
response += item.sys_id;
response += '\')" title="';
response += helpText;
response += '" data-original-title="';
response += helpText;
response += '">Comment</a>';
return response;
},
getLastJournalEntry: function(sys_id) {
var journalGR = new GlideRecord('sys_journal_field');
journalGR.orderByDesc('sys_created_on');
journalGR.addQuery('element_id', sys_id);
journalGR.setLimit(1);
journalGR.query();
journalGR.next();
return journalGR;
},
type: 'LastCommentValueProvider'
};
Basically, we go grab the last comment, and look at the author and the date to determine both the class name and the associated help text for the button. Once that has been determined, then we build the HTML to produce the button in a similar fashion to the buttons created using the button/icon configuration. Those of you paying close attention will notice that the one significant difference between this HTML and the HTML produced from a button/icon configuration is the use of onClick in the place of ng-click. This has to be done because the HTML added to the page for a scripted value column is not compiled, so an ng-click will not work. The problem with an onClick, though, is that it is outside the scope of the widget, so we have to add this little tidbit of script to the HTML of our companion widget to address that.
<div>
<script>
function tempFunction(elem, sys_id) {
var scope = angular.element(elem).scope();
scope.$apply(function() {
scope.buttonClick('comment', {sys_id: sys_id});
});
}
</script>
</div>
This brings things full circle and gets us back inside the scope of the primary widget to activate the normal button click process, which will send out the $rootScope broadcast message, which will in turn get picked up by our companion widget. Normally, the HTML for a companion widget would be completely empty, but in this particular case, we were able to leverage that section to insert our little client script. I plan to bundle all of these artifacts up into an Update Set so that folks can play around with them, but before I do that, I wanted to throw out a couple more examples. We will take a look at another one of those next time out.
“The power of one, if fearless and focused, is formidable, but the power of many working together is better.” — Gloria Macapagal Arroyo
Last time, we released yet another beta version of the app for testing, so now might be a good time to talk a little bit about what exactly needs to be tested, and maybe a little bit about where things stand and where we go from here. We have had a lot of good, quality feedback in the past, and I am hoping for even more from this version. Every bit helps drive out annoying errors and improves the quality of the product, so keep it coming. It is very much appreciated.
Installation
The first thing that needs to be tested, of course, is the installation itself. Just to review, you need to install three Update Sets in the appropriate order, SNH Form Fields, the primary Scoped Application, and the accompanying global components that could not be bundled with the scoped app. You can find the latest version of SNH Form Fieldshere, or you can simply grab the latest SNH Data Table Widgets from Share, which includes the latest version of the form field tag. Once that has been installed, you can then install collaboration_store_v0.7.5.xml, after which you can then install the globals, collaboration_store_globals_v0.7.xml.
There are two types of installations, a brand new installation and an upgrade of an existing installation. Both types of installs have had various issues reported with both Preview and Commit errors. On a brand new installation, just accept all updates and you should be good to go. I don’t actually know why some of those errors come up on a brand new install, but if anyone knows of any way to keep that from happening, I would love to hear about it. It doesn’t seem to hurt anything, but it would be so much better if those wouldn’t come up at all. On an upgrade to an existing installation, you will want to reject any updates related to system properties. The value of the application’s properties are established during the set-up process, and if you have already gone through the set-up process, you don’t want those values overlaid by the installation of a new version. Everything else can be accepted as is. Once again, if anyone has any ideas on how to prevent that kind of thing from happening, please let us all know in the comments below.
The Set-up Process
Once you have the software installed for the first time, you will need to go through the set-up process. This is another thing that needs to be tested thoroughly, both for a Host instance and a Client instance. It needs to tested with logo images and without, and for Client instances, you will need to check all of the other member instances in the community to ensure that the newly set-up instance now appears in each instance. During the set-up process, a verification email will be sent to the email address entered on the form, and if your instance does not actually send out mail, you will need to look in the system email logs for the code that you will need to complete the process.
The Publishing Process
Once the software has been installed and the set-up process completed, you can now publish an application to the store. Both Client instances and Host instances can publish apps to the store. Publishing is accomplished on the system application form via a UI Action link at the bottom of the form labeled Publish to Application Store. Click on the link and follow the prompts to publish your application to the store. If you run into any issues, please report them in the comments below.
The Installation Process
Once published to the store, shared applications can be installed by any other Host or Client instance from either the version record of the version desired, or the Collaboration Store itself. Simply click on the Install button and the installation should proceed. Once again, if you run into any issues, please use the comments below to provide us with some detailed information on where things went wrong.
The Collaboration Store
The Collaboration Store page is where you should see all of the applications shared with the community and there are a lot of things that need to be tested here. This is the newest addition to the application, so we will want to test this thing out thoroughly. One thing that hasn’t been tested at all is the paging, as I have never shared enough apps to my own test environment to exceed the page limit. The more the merrier as far as testing is concerned, so if you can add as many Client instances as possible, that would be helpful, and if each Client could share as many applications as possible, that would be helpful as well. Several pages worth, in varying states would help in the testing of the search widget as well as the primary store widget. And again, if you run into any problems, please report them in the comments.
The Periodic Sync Process
The periodic sync process is designed to recover from any previous errors and ensure that all Clients in the community have all of the same information that is stored in the Host. Testing the sync process is simply a matter of removing some artifacts from some Client instance and then running the sync process to see if those artifacts were restored. The sync process runs every day on the Host instance over the lunch hour, but you can also pull up the Flow and run it by hand.
Thanks in advance to those of you who have already contributed to the testing and especially to those of you who have decided to jump in at this late stage and give things a try. Your feedback continues to be quite helpful, and even if you don’t run into any issues, please leave us a comment and let us know that as well. Hopefully, we are nearing the end of this long, drawn out project, and your assistance will definitely help to wrap things up. Next time, we will talk a little bit more about where things go from here.
Last time, we built a little companion widget to share the page with our storefront widget to provide the ability to filter the list of applications displayed. Clicking on the search button on that widget reloads the page with the search criteria present in the query string of the URL. Now we need to modify the primary widget to pull that search criteria in from the URL and then use it when querying the database for applications to display. To begin, we can use basically the same code that we used in the search widget to bring in the values.
var search = $sp.getParameter('search');
var local = $sp.getParameter('local') == 'true';
var current = $sp.getParameter('current') == 'true';
var upgrade = $sp.getParameter('upgrade') == 'true';
var notins = $sp.getParameter('notins') == 'true';
Now that we have the information, we need to use it to filter the query results. For the search string, we want to look for that string in either the name or the description of the app. That one is relatively straightforward and can be handled with a single encoded query.
var appGR = new GlideRecord('x_11556_col_store_member_application');
if (search) {
appGR.addEncodedQuery('nameLIKE' + search + '^ORdescriptionLIKE' + search);
}
The remaining parameters are all boolean values that relate to one another in some way, so we have to handle them as a group. First of all, if none of the boxes are checked or all of the boxes are checked, then there is no need for any filtering, so we can eliminate those cases right at the top.
if (local && current && upgrade && notins) {
// everything checked -- no filter needed
} else if (!local && !current && !upgrade && !notins) {
// nothing checked -- no filter needed
} else {
...
}
After that, things get a little more complicated. Local apps are those where the provider instance is the local instance, and we don’t apply any other filter to that pool. You either want the local apps included or you do not. They are included by default, but if you start checking boxes then they are only included if you check the Local checkbox.
The remaining three values relate to the apps pulled down from the store, which are classified based on whether they have been installed on the local instance (current and upgrade) or not (notins). Installed apps have a value in the application field and those that have not been installed do not. Additionally, installed apps are considered current if the installed version is the same as the current version; otherwise they are classified as having an upgrade available. We only need to check the version if you want one, but not the other. If you want both current and upgrade or neither current nor upgrade, then there is no point in making that distinction. So we first check both cases where the values are different.
That should do it. Putting that all together with the original fetchItemDetails function gives us this new version of that function.
function fetchItemDetails(items) {
var search = $sp.getParameter('search');
var local = $sp.getParameter('local') == 'true';
var current = $sp.getParameter('current') == 'true';
var upgrade = $sp.getParameter('upgrade') == 'true';
var notins = $sp.getParameter('notins') == 'true';
var appGR = new GlideRecord('x_11556_col_store_member_application');
if (search) {
appGR.addEncodedQuery('nameLIKE' + search + '^ORdescriptionLIKE' + search);
}
if (local && current && upgrade && notins) {
// everything checked -- no filter needed
} else if (!local && !current && !upgrade && !notins) {
// nothing checked -- no filter needed
} else {
var query = '';
var separator = '';
if (local) {
query += 'provider.instance=' + gs.getProperty('instance_name');
separator = '^OR';
} else {
query += separator + 'provider.instance!=' + gs.getProperty('instance_name');
separator = '^';
}
if (current && !upgrade) {
if (notins) {
query += separator + 'applicationISEMPTY^ORversionSAMEASapplication.version';
} else {
query += separator + 'versionSAMEASapplication.version';
}
} else if (!current && upgrade) {
if (notins) {
query += separator + 'applicationISEMPTY^ORversionNSAMEASapplication.version';
} else {
query += separator + 'versionNSAMEASapplication.version';
}
} else if (current && upgrade && !notins) {
query += separator + 'applicationISNOTEMPTY';
} else if (!current && !upgrade && notins) {
query += separator + 'applicationISEMPTY';
}
appGR.addEncodedQuery(query);
}
appGR.orderBy('name');
appGR.query();
while (appGR.next()) {
var item = {};
item.name = appGR.getDisplayValue('name');
item.description = appGR.getDisplayValue('description');
item.logo = appGR.getValue('logo');
item.version = appGR.getDisplayValue('current_version');
item.provider = appGR.getDisplayValue('provider.name');
item.providerLogo = appGR.provider.getRefRecord().getValue('logo');
item.local = appGR.getDisplayValue('provider.instance') == gs.getProperty('instance_name');
item.sys_id = appGR.getUniqueValue();
item.state = 0;
if (appGR.getValue('application')) {
item.state = 1;
item.installedVersion = appGR.getDisplayValue('application.version');
if (item.version == item.installedVersion) {
item.state = 2;
}
}
if (!item.local && item.state != 2) {
item.attachmentId = getAttachmentId(item.sys_id, item.version);
}
items.push(item);
}
}
Now we can fire up the storefront page and start clicking around and see what we have. Or better yet, we can push out yet another Update Set and let all of you following along at home click around and see if everything works as it should. I always like it when folks with a little different perspective take the time to pull stuff down and give it a whirl, so here you go:
If this is your first time, you will want to take a peek here and here and here. For the rest of you, this is just another 0.7.x drop-in replacement, and you should know what to do by now. Please let us all know what you find in the comments below. Feedback is always welcome and always very much appreciated. Hopefully, we will get some interesting results and we can take a look at those next time out.