“Our life is frittered away by detail. Simplify, simplify.” — Henry David Thoreau
There are two distinctly different environments on ServiceNow, the original ServiceNow UI based on Lists, Forms, and UI Pages, and the more recent Service Portal environment based on Widgets and Portal Pages. The foundation technology for UI Pages is Apache Jelly. The foundation technology for Service Portal Widgets is AngularJS. From a technology perspective, the two really aren’t all that compatible; when you are working with the legacy UI, you work in one technology and when you are working with the Service Portal, you work in the other.
Personally, I like AngularJS better than Jelly, although I have to admit that I am no expert in either one. Still, given the choice, my preferences always seem to tilt more towards the AngularJS side of the scale. So when I had to build a modal pop-up for a UI Page, my inclination was to find a way to do it using a Service Portal Widget. To be completely honest, I wanted to find a way to be able to always use a widget for any modal pop-up on the ServiceNow UI side of things. That way, I could hone my expertise on just one approach, and be able to use it on both sides of the house.
The solution actually turned out to be quite simple: I created a very simple UI Page that contained just a single element, an iframe. I passed just one parameter to the page, and that was the URL that was to be the source for the iframe. Now, that URL could contain multiple URL parameters to be passed to the interior widget, but from the perspective of the UI Page itself, all you are passing in is the lone URL, which is then used to establish the value of the src attribute of the iframe tag. There are two simple parts to the page, the HTML and the associated script. Here is the HTML:
var gdw = GlideDialogWindow.get();
var url = gdw.getPreference('url');
document.getElementById('portal_page_frame').src = url;
The script simply gets a handle on the GlideDialogWindow so that it can snag the URL “preference”, and then uses that value to establish the source of the iframe via the iframe’s src attribute. To pop open a Service Portal Widget using this lightweight portal_page_container, you would use something like the following:
var dialog = new GlideDialogWindow("portal_page_container");
dialog.setPreference('url', '/path/to/my/widget');
dialog.render();
That’s it. Simple. Easy. Quick. Just the way I like it! If you want a copy to play with, you can get it here.
Update: There is an even better way to do this, which you can find here.
“Some people think design means how it looks. But of course, if you dig deeper, it’s really how it works.” — Steve Jobs
The other day I was trying to remember how to do something on the Service Portal, and it occurred to me that it would be nice to have a little pop-up help screen on all of my various custom widgets so that I could put a little help text behind each one in a consistent manner. There is already an Angular Provider called snPanel that provides a set of stock wrappings for a widget, so it seemed like it wouldn’t take too much to add a little something to that guy to put some kind of Help icon in a consistent place on the title bar, say a question mark in the upper right-hand corner:
Of course, placing the icon on the screen is just the start; setting it up to do something in a way that was useful and easy to maintain would be the bulk of the work. A modal dialogue seemed to be the best vehicle to deliver the content, but the content would need to come from somewhere, and be somehow linked to the widget in a way that did not require extra code or special logic. After considering a number of various alternatives, I decided to create a new Knowledge Base called User Help for the specific purpose of housing the content for my new help infrastructure. I didn’t want to have to do anything special to a widget in order for the help to be available, so my plan was to use the Title property of the Knowledge Article to store the name or ID of the widget, linking the article directly to the widget without having to store any kind of help ID on the widget itself. This would allow me to create help text for any widget by simply putting some kind of link to the existing widget in the title of the article, which would not actually appear on the screen in my proposed help text modal pop-up window.
So for now, let’s just assume that we can modify the snPanel to include a link to a modal pop-up and focus on the widget that we will use to display the help content. To make ongoing maintenance easier, in addition to the help text itself, it would also be quite handy to have a link to the editor on the screen for those with the power to edit the help text. The link would be slightly different depending on whether or not there was already some help text in existence for this widget, but that’s easy enough to handle with a little bit of standard widget HTML:
<div ng-bind-html="c.data.html"></div>
<div ng-show="c.data.contentEditor && c.data.sysId" style="padding: 20px; text-align: center;">
<a class="btn btn-primary" href="/nav_to.do?uri=%2Fkb_knowledge.do%3Fsys_id%3D{{c.data.sysId}}" target="_blank">Edit this Help content</a>
</div>
<div ng-show="c.data.contentEditor && !c.data.sysId" style="padding: 20px; text-align: center;">
<a class="btn btn-primary" href="/nav_to.do?uri=%2Fkb_knowledge.do%3Fsys_id%3D-1%26sysparm_query%3Dkb_knowledge_base%3D{{c.data.defaultKnowledgeBase}}%5Ekb_category%3D{{c.data.defaultCategory}}%5Eshort_description%3D{{c.data.id}}" target="_blank">Create Help content for this widget</a>
</div>
The first DIV is for the content and the next two are mutually exclusive, if they even show up at all. If you are not a content editor, you won’t see either one, but if you are, you will see the Edit this Help content link if help text exists for this widget and the Create Help content for this widget link if it does not.
We shouldn’t need any client side code at all for this simple widget, and the server side should be fairly simple as well: go fetch the help content and figure out if the current user can edit content or not. For our little example, let’s just limit editing to users with the admin role, and assume that the default knowledge base and default category are both called User Help.
(function() {
if (!data.defaultKnowledgeBase) {
fetchDefaultKnowledgeBase();
}
if (!data.defaultCategory) {
fetchDefaultCategory();
}
data.contentEditor = false;
if (gs.hasRole('admin')) {
data.contentEditor = true;
}
if (input && input.id) {
data.id = input.id;
if (!data.html) {
data.html = fetchHelpText();
}
}
function fetchHelpText() {
data.sysId = false;
var html = '<p>There is no Help available for this function.</p>';
var help = new GlideRecord('kb_knowledge');
help.addQuery('kb_knowledge_base', data.defaultKnowledgeBase);
help.addQuery('kb_category', data.defaultCategory);
help.addQuery('short_description', data.id);
help.addQuery('workflow_state', 'published');
help.query();
if (help.next()) {
data.sysId = help.getValue('sys_id');
html = help.getDisplayValue('text');
}
return html;
}
function fetchDefaultKnowledgeBase() {
var gr = new GlideRecord('kb_knowledge_base');
gr.addQuery('title', 'User Help');
gr.query();
if (gr.next()) {
data.defaultKnowledgeBase = gr.getValue('sys_id');
}
}
function fetchDefaultCategory() {
var gr = new GlideRecord('kb_category');
gr.addQuery('kb_knowledge_base', data.defaultKnowledgeBase);
gr.addQuery('label', 'User Help');
gr.query();
if (gr.next()) {
data.defaultCategory = gr.getValue('sys_id');
}
}
})();
The script includes three independent functions, one to fetch the sys_id of the default knowledge base, one to fetch the sys_id of the default category, and one to use those two bits of data, plus the widget‘s ID, to fetch the help content. There are three hard-coded values in this example that would be excellent candidates for System Properties: the Knowledge Base, the Knowledge Category, and Role or Roles used to identify content editors. For now, though, I just plugged in specific values to simplify the example. That’s pretty much it for the pop-up modal dialogue’s widget. Now we just need to hack up the snPanel code to wedge in our link. We should be able to do that by inserting another bit of HTML inside of the h2 heading tag:
<div class="pull-right">
<a href class="h4" title="Click here for Help" ng-click="widgetHelp()">
<span class="m-r-sm fa ng-scope fa-question"></span>
</a>
</div>
The ng-click value in the anchor tag references a scoped function, so we’ll need to add a controller to the provider so that we can insert the code for that function. This is the code that will run whenever someone clicks on our new fa-question icon.
The function basically contains a single line of code, which is your standard spModal open command using all of the usual parameters, plus passing in the widget‘s ID as widgetInput, which will be used as the key to retrieve the associated help text from the default Knowledge Base. This actually turned out to be a little more of a modification than it would care to make to a standard ServiceNow component such as snPanel, so I ended up creating a copy of my own and producing the snhPanel, which can now be used anywhere that snPanel can be used. All told, we created one widget and one Angular Provider to make all of this work, and then configured a Knowledge Base and Knowledge Category to house the help text content created through this new user help infrastructure. There are only a couple of parts to this one, but if anyone is interested, here is an Update Set that contains both of them.
On the stock header of a ServiceNow portal there is an image of a shopping cart. As you order things from the Service Catalog, the number of items in your cart is displayed in the lower right-hand corner of that image. If you didn’t come to the portal to order something, the empty cart just sits there at the top of the screen for no reason, so I thought it might be nice to just hide the cart until there was at least one item in it.
There are already a couple of other items in the header that only appear if there is something there, the list of Requests and the the list of Approvals. I figured that I could emulate the approach taken on those items and it would work the same way.
Most of the things that drive ServiceNow are built using ServiceNow, and you can find them in the system database and edit them to do what you want. Some things, however, are built into the system, and there is no way that you can modify them. There is also a third category, and that is those things that actually are housed in the system database, but are locked down as read-only or ACL-controlled such that you cannot modify them. The Header Menu widget that contains the shopping cart code happens to fall into that third category. There it was right in front of me, but it was not editable, even though I am a System Administrator and I was in the right Update Set.
Nothing motivates me more than being told that I am being prevented from doing something that I want to do, so I immediately went to work trying to figure out a way to make changes to the Header Menu widget. As it turns out, it wasn’t that hard to do.
The first thing to do was to use the hamburger menu to export the widget to XML:
Once I had my own copy on my own machine, I could look at the code and figure out what need to be changed and then change it. Digging through the HTML, I found out that there was already an ng-show attribute on the shopping cart for another purpose, so I just needed to add my condition to the list, and that should do the trick. Here is the relevant section of code:
A little further down in the code there was reference to a variable called cartItemCount. Using my puissant powers of deductive reasoning, I concluded, based on the name of the variable and its usage, that this variable contained the number of items in the cart. This is exactly what I needed for my condition, so I changed this line:
Now that I have figured out what change to make and have implemented the change, all I need to do is get it back into the instance. Fortunately, ServiceNow provides a way to do that, and it has the added benefit of bypassing those pesky rules that were preventing me from editing the thing directly in the tool.
If you open the System Update Sets section and go all the way down to the bottom and select Update Sets to Commit, there will be a link down at the bottom of that page titled Import Update Set from XML. Click on that guy, and even though your exported and modified XML document is not technically an Update Set, you can pull it in via that process and it will update the widget with your changes.
Now when I am on a portal that uses the Header Menu widget, you only see the Shopping Cart when there is something in the cart. Whenever it is empty, it drops out of the view.