Generic Feedback Widget, Part V

“If at first you don’t succeed, you are running about average.”
M.H. Alderson

I looked at several different ways to solve my problem with the Generic Feedback Widget, but I couldn’t come up with anything that didn’t involve inactivating or altering the ACL that was at the heart of the issue.Finally, I settled on a plan that would at least involve minimally invasive alterations to the ACL. The plan was pretty simple: create an obscure User Preference and set it to true just before accessing the live_group_profile record, and then delete the preference as soon as the record was obtained. The alteration to the ACL, then, would be to check for that preference before applying the ACL. The updated version of the ACL script now looked like this:

if (gs.getPreference('snh.live.group.read.authorization') == 'true') {
	answer = true;
} else {
	var gr = new GlideRecord('live_group_member');
	gr.addQuery('member', GlideappLiveProfile().getID());
	gr.addQuery('group', current.sys_id);
	gr.addQuery('state', 'admin').addOrCondition('state', 'active');
	gr.query();
	answer = gr.next();
}

The first thing that we do is check for the preference, and if it’s there, then we bypass the original code; otherwise, things proceed as they always have. I don’t really like tinkering with stock components if I can avoid it, mainly because of the subsequent issues with patches and upgrades potentially skipping an upgrade of anything that you have touched. Still, this one seemed to be unavoidable if I wanted to salvage the original intent and still do what I wanted to do.

The next thing that I needed to do was to set the preference just before attempting the read operation, and then removing it as soon as I was done. That code turned out to look like this:

gs.getUser().setPreference('snh.live.group.read.authorization', 'true');
grp = new GlideRecord('live_group_profile');
grp.addQuery('table', table);
grp.addQuery('document', sys_id);
grp.query();
if (grp.next()) {
	groupId = grp.getValue('sys_id');
}
gs.getUser().setPreference('snh.live.group.read.authorization', null);

I ended up pulling that out of the widget and putting it into its own Script Include, mainly to tuck the specialized code away and out of sight. Anyway, it all sounded like a great plan and all I needed to do now was to test it out, so I did. And it failed. So much for my great plan.

It took a little digging, but I finally figured out that the ACL was not the only thing keeping people from outside the group from reading the group profile record. There are also a number of Business Rules that do pretty much the same thing. I spent a little time combing through all of those looking for ways to hack around them, and then finally decided that, for my purposes anyway, I really didn’t to be running any Business Rules at all. So I added one more line to my read script to turn off all of the Business Rules.

gs.getUser().setPreference('snh.live.group.read.authorization', 'true');
grp = new GlideRecord('live_group_profile');
grp.setWorkflow(false);
grp.addQuery('table', table);
grp.addQuery('document', sys_id);
grp.query();
if (grp.next()) {
	groupId = grp.getValue('sys_id');
}
gs.getUser().setPreference('snh.live.group.read.authorization', null);

That did it. Now, people who are not in the group can still read the group profile record, which is good, because you need the sys_id of that record to read all of the messages in the group, which is what we are using as feedback. The only thing that I have accommodated at this point is situations where a group profile record does not exist at all, and I have to create one.

But that’s an entirely different adventure

Dynamic Service Portal Breadcrumbs

“Do not wait; the time will never be ‘just right.’ Start where you stand, and work with whatever tools you may have at your command, and better tools will be found as you go along.”
George Herbert

I’ve had this idea for a while to attempt a different approach to Service Portal breadcrumbs, and I finally quit tinkering with my Data Table clones and Configurable Content Selector long enough to actually throw something together. My issue with the out-of-the-box breadcrumb widget is that you have to tell it what the breadcrumbs are on every page rather than the system keeping track of where you are and how you got there. It seemed to me that it would not only be easier to set up, but it would also be more accurate, since there are often times more than one path to get to a specific page.

To keep track of the current page stack for the breadcrumbs, I decided to leverage the existing User Preferences infrastructure. User Preferences are accessible in the Service Portal via built-in GlideSystem functions, and provide a convenient means to keep track of a user’s path through the various screens in the portal. To fetch a User Preference, you use the gs.getPreference(key) method, and to update a User Preference, the script is gs.getUser().setPreference(key, value).

To begin, I pulled up the existing breadcrumb widget and created a clone that I called SNH Breadcrumbs. I did not want to change the way the breadcrumbs were displayed, so I left the HTML portion of the widget intact. I did not want to set the value of the breadcrumbs via widget option anymore, though, so I removed the option. Then I modified the server-side script to create a label for the current page and pull the current breadcrumbs out of the User Preferences. I also provided the means to update the breadcrumbs when an update was invoked on the client side. The complete server-side script now looks like this:

(function() {
	if (input) {
		if (input.breadcrumbs) {
			gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
		}
	} else {
		data.table = $sp.getParameter('table');
		data.sys_id = $sp.getParameter('sys_id');
		if (data.table) {
			var rec = new GlideRecord(data.table);
			if (data.sys_id) {
				rec.get(data.sys_id);
				data.page = rec.getDisplayValue('number');
				if (!data.page) {
					data.page = rec.getDisplayValue('name');
				}
				if (!data.page) {
					data.page = rec.getDisplayValue('short_description');
				}
				if (!data.page) {
					data.page = rec.getLabel();
				}
			} else {
				data.page = rec.getPlural();
			}
		}
		data.breadcrumbs = [];
		var snhbc = gs.getPreference('snhbc');
		if (snhbc) {
			data.breadcrumbs = JSON.parse(snhbc);
		}
	}
})();

The page label is based on the URL parameters table and sys_id. If both are present, I go ahead and grab the record and attempt to obtain a label from the data. If only the table parameter is present, then I assume that we are talking about multiple records, so I grab the Plural label for the table itself. If there is no table parameter, then I let the client-side script handle the label for the page. On the client side, I build a breadcrumb entry for the current page, and then loop through the existing breadcrumbs to see if this page is already in the list. If it is, then that’s where we will stop; otherwise, we will just tack the new current page entry on to the end of the existing stack of pages. Here is the complete client-side script:

function($scope, $rootScope, $location, spUtil) {
	var c = this;
	c.expanded = !spUtil.isMobile();
	c.expand = function() {
		c.expanded = true;
	};
	c.breadcrumbs = [];
	var thisPage = {url: $location.url(), id: $location.search()['id'], label: c.data.page || document.title};
	
	if (thisPage.id != $rootScope.portal.homepage_dv) {
		var pageFound = false;
		for (var i=0;i<c.data.breadcrumbs.length && !pageFound; i++) {
			if (c.data.breadcrumbs[i].id == thisPage.id) {
				c.breadcrumbs.push(thisPage);
				pageFound = true;
			} else {
				c.breadcrumbs.push(c.data.breadcrumbs[i]);
			}
		}
		if (!pageFound) {
			c.breadcrumbs.push(thisPage);
		}
	}
	c.data.breadcrumbs = c.breadcrumbs;
	c.server.update();
}

That’s really all there is to it. Here’s one example of how it looks in practice:

Dynamic breadcrumbs example

In the example above, the URL for the page contains a table parameter, but no sys_id parameter. This generates a page label from the table’s getPlural() method. If we select a different perspective, which uses a different table, we will still be on the same page, but the page label will reflect the current table in use for that perspective.

Breadcrumbs example using a different perspective/table

Now, if you click on one of the items in the table, you will see that the breadcrumb list grows, and this time the URL has both a table and a sys_id parameter, but the record in question (sysapproval_approver) has no number, name, or short_description fields, so the label is defaulted to the generic label for the record.

Approval record breadcrumb example

Clicking on the Approvals breadcrumb will take you back to the original screen, removing the single Approval record from the breadcrumb array.

Using the breadcrumb to return to the initial screen

Now, if you click on the Change record instead of the Approval record, the Change record actually does have a number field, so the label for that page is the actual number of the record.

Breadcrumbs example with numbered record

And finally, if you click on the Opened by column, which is configured to take you to the User Profile page, there is no number, but there is a name, so that becomes the label.

Breadcrumbs example from the Opened by column

The reason that you can find the record to fetch the name in the above example is because the Data Table widget arbitrarily passes both the table and sys_id parameters to the User Profile page, even though table is not needed (the table sys_user is assumed by the User Profile page — you don’t have to pass it). When you pull down the User menu and select Profile, no table name is passed in the URL, so the label defaults to the name of the page.

Breadcrumbs example from the User Profile selection

One thing to keep in mind is that the trail will only build as you pass through pages that contain the widget. Any pages that you pass through that do not contain the widget will not get added to the running list of pages, as there will be no code running that pushes the page onto the stack. Other than that, it seems to work in most other cases. If you want to try it out for yourself, here’s an Update Set that contains the custom widget.

Update: There is a better (working!) version here.