Service Portal Widget Help

“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:

Possible User Help Icon

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.

controller: function($scope, spModal) {
	$scope.widgetHelp = function() {
		spModal.open({
			title: 'User Help',
			widget: 'snh-help',
			widgetInput: {id: $scope.widget.id},
			buttons: [
				{label: 'Close', primary: true}
			],
			size: 'lg'
		});
	};
},

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.