“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.
“It’s really complex to make something simple.” — Jack Dorsey
Last time, we wrapped up an initial version of the storefront and released a new Update Set in the hopes that a few folks out there would pull it down and take it for a spin. While we wait patiently to hear back from anyone who might have been willing to download the new version and run it through its paces, let’s see if we can’t add a little more functionality to our shopping experience. The page that we laid out had a place for two widgets, but we only completed the primary display widget so far. The other, smaller portion of the screen was reserved for some kind of search/filter widget to help narrow down the results for installations where a large number of applications have been shared with the community. Adding that second widget to the page would give us something like this:
Rather than have the two widgets speak directly to one another, my thought was that we could use the same technique that was used with the Configurable Data Table Widget Content Selector bundled with the SNH Data Table Widgets. Instead of having one widget broadcast messages and the other widget listen for those messages, the Content Selector communicates with the Data Table widget via the URL. Whenever a selection is made, a URL is constructed from the selections, and then the Content Selector branches to that URL where both the Content Selector and the Data Table widget pull their control information from the shared URL query parameters. We can do exactly the same thing here for the same reasons.
For this initial attempt, I laid out a generic search box and a number of checkboxes for various states of applications on the local instance. To support that, we can use the following URL parameters: search, local, current, upgrade, and notins. In the widget, we can also use the same names for the variables used to store the information, and we can populate the variables from the URL. In fact, that turns out to be the entirety of the server-side code.
There is one client-side function referenced in the HTML for the button click, and that’s pretty much all there is to the widget’s Client script.
api.controller = function($scope, $location) {
var c = this;
$scope.search = function() {
var search = '?id=' + $location.search()['id'];
if (c.data.search) {
search += '&search=' + encodeURIComponent(c.data.search);
}
if (c.data.local) {
search += '&local=true';
}
if (c.data.current) {
search += '¤t=true';
}
if (c.data.upgrade) {
search += '&upgrade=true';
}
if (c.data.notins) {
search += '¬ins=true';
}
window.location.search = search;
};
};
The search() function builds up a URL query string based on the operator’s input and then updates the current location with the new query string, essentially branching to the new location. When the new page loads, both the search widget and the storefront widget can then pull their information from the current URL. We can test all of this out now by saving the new widget, pulling up our collaboration_store page in the Service Portal Designer, and then dragging our new widget onto the page in the space already reserved for that purpose.
With that completed, we can now try out the page and see that entering some data and clicking on the Search button actually does reload the page with a new URL. However, at this point the content of the primary page never changes because we have yet to add code to the main widget to pull in the URL parameters and then use that data to adjust the database query. That sounds like a good subject for our next installment.
“The trouble with programmers is that you can never tell what a programmer is doing until it’s too late.” — Seymour Cray
Last time, we started building a widget for the application details pop-up and today we need to wrap that up. We left off with a rough layout of what the pop-up might contain, and now we need to gather up all of the data necessary to populate the screen. The first thing that we need to do is get the primary application record.
Then we need the functions that fetch the attachment ID and all of the version records.
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;
}
function getVersionRecords(applicationId) {
var versionList = [];
var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
versionGR.addQuery('member_application', applicationId);
versionGR.orderByDesc('sys_created_on');
versionGR.query();
while (versionGR.next()) {
var thisVersion = {};
thisVersion.date = formatDate(versionGR.getDisplayValue('sys_created_on'));
thisVersion.builtOn = versionGR.getDisplayValue('built_on');
thisVersion.version = versionGR.getDisplayValue('version');
versionList.push(thisVersion);
}
return versionList;
}
The version records are dated, and the date format that I have chosen is month day, year (‘MMM d, yyyy’); however, for today’s date and yesterday’s date, I replace the date with the words Today and Yesterday. To pull that off, I need to create some variables for those two dates right at the top.
var gd = new GlideDate();
var today = gd.getByFormat('MMM d, yyyy');
var gdt = new GlideDateTime();
gdt.addDaysLocalTime(-1);
gd.setValue(gdt.getDate());
var yesterday = gd.getByFormat('MMM d, yyyy');
Once those values have been establish, I can reference them in the date format function.
function formatDate(dateString) {
var response = '';
if (dateString) {
var date = new GlideDate();
date.setValue(dateString);
response = date.getByFormat('MMM d, yyyy');
if (response == today) {
response = 'Today';
} else if (response == yesterday) {
response = 'Yesterday';
}
}
return response;
}
That’s pretty much it for the server side code. Here is the whole thing all put together.
(function() {
var gd = new GlideDate();
var today = gd.getByFormat('MMM d, yyyy');
var gdt = new GlideDateTime();
gdt.addDaysLocalTime(-1);
gd.setValue(gdt.getDate());
var yesterday = gd.getByFormat('MMM d, yyyy');
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.applicationId = appGR.getValue('application');
data.record.logo = appGR.getValue('logo');
data.record.version = appGR.getDisplayValue('current_version');
data.record.provider = appGR.getDisplayValue('provider.name');
data.record.providerId = appGR.getValue('provider');
data.record.providerLogo = appGR.provider.getRefRecord().getValue('logo');
data.record.local = appGR.getDisplayValue('provider.instance') == gs.getProperty('instance_name');
data.record.state = 0;
if (data.record.applicationId) {
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);
}
data.record.versionList = getVersionRecords(data.sysId);
}
}
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;
}
function getVersionRecords(applicationId) {
var versionList = [];
var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
versionGR.addQuery('member_application', applicationId);
versionGR.orderByDesc('sys_created_on');
versionGR.query();
while (versionGR.next()) {
var thisVersion = {};
thisVersion.date = formatDate(versionGR.getDisplayValue('sys_created_on'));
thisVersion.builtOn = versionGR.getDisplayValue('built_on');
thisVersion.version = versionGR.getDisplayValue('version');
versionList.push(thisVersion);
}
return versionList;
}
function formatDate(dateString) {
var response = '';
if (dateString) {
var date = new GlideDate();
date.setValue(dateString);
response = date.getByFormat('MMM d, yyyy');
if (response == today) {
response = 'Today';
} else if (response == yesterday) {
response = 'Yesterday';
}
}
return response;
}
})();
To format all of this data, we use the following HTML.
This is another drop-in replacement for any previous 0.7.x version. If you have been already been testing with any other version, just install this one over the one that you have been using. If you installing for the first time, you will need the other prerequisites, which you can read about here and here and here. As always, feedback of any kind in the comments section is welcome, encouraged, and very much appreciated. Also, any ideas on the shopping experience in general, or on the search widget that we have yet to add to the other side of the page, would be great as well. Next time, we may start taking a look at that unless we have some test results to review. Thanks to everyone who has taken the time to take this out for a spin, and if you haven’t done it yet, please give it a try and let us know what you find.