Companion Widgets for SNH Data Tables, Part III

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

A list of requests that includes a count of requested items

Here is the configuration script used to produce this table:

var MyRequestsConfig = Class.create();
MyRequestsConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'all',
		label: 'All',
		roles: ''
	}],

	state: [{
		name: 'all',
		label: 'All'
	}],

	table: {
		all: [{
			name: 'sc_request',
			displayName: 'Request',
			all: {
				filter: 'requested_forDYNAMIC90d1921e5f510100a9ad2572f2b477fe^active=true',
				fields: 'number,opened_at,short_description,stage,sys_updated_on',
				svcarray: [],
				aggarray: [{
					name: 'items',
					label: 'Items',
					heading: '',
					table: 'sc_req_item',
					field: 'request',
					filter: '',
					source: '',
					hint: 'Click here to view the items in this request',
					page_id: ''
				}],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			}
		}]
	},

	type: 'MyRequestsConfig'
});

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.

Modal pop-up of requested items

Clicking on an item should bring up the ticket page, where you can see more details about the item.

Requested Item details

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 spModal open() 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.

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.aggregateClick, function(e, parms) {
		var modelOptions = {
			title: "${Requested Items}",
			widget: "widget-modal-list",
			widgetInput: {
				options: {
					table: 'sc_req_item',
					filter: 'request=' + parms.record.sys_id,
					display_field: 'number',
					order_by: 'number',
					image_field: 'cat_item.picture',
					secondary_fields: 'stage,cat_item.name',
					sp_page: 'ticket'
				}
			},
			buttons: [
				{label: '${Done}', cancel: true}
			],
			size: 'md'
		};
		spModal.open(modelOptions);
	});
};

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!

Companion Widgets for SNH Data Tables, Part II

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

My Approvals example page

Here is the configuration script that we will used to produce this table.

var MyApprovalsConfig = Class.create();
MyApprovalsConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'all',
		label: 'All',
		roles: ''
	}],

	state: [{
		name: 'all',
		label: 'All'
	}],

	table: {
		all: [{
			name: 'sysapproval_approver',
			displayName: 'Approval',
			all: {
				filter: 'approverDYNAMIC90d1921e5f510100a9ad2572f2b477fe^state=requested',
				fields: 'sysapproval.sys_class_name,sysapproval,sysapproval.short_description,sysapproval.opened_by',
				svcarray: [],
				aggarray: [],
				btnarray: [{
					name: 'approve',
					label: 'Approve',
					heading: 'Approve',
					icon: 'workflow-approved',
					color: 'success',
					hint: 'Click here to approve',
					page_id: '',
					condition: ''
				},{
					name: 'approvecmt',
					label: 'Approve w/Comments',
					heading: 'w/Comments',
					icon: 'comment-hollow',
					color: 'success',
					hint: 'Click here to approve with comments',
					page_id: '',
					condition: ''
				},{
					name: 'reject',
					label: 'Reject',
					heading: 'Reject',
					icon: 'workflow-rejected',
					color: 'danger',
					hint: 'Click here to reject',
					page_id: '',
					condition: ''
				}],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: [{
					name: 'approve',
					label: 'Approve'
				},{
					name: 'approvecmt',
					label: 'Approve w/Comments'
				},{
					name: 'reject',
					label: 'Reject'
				}]
			}
		}]
	},

	type: 'MyApprovalsConfig'
});

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.

Entering comments for the selected action

Once the comments are entered, the companion widget should go ahead and update the database and inform the user of the action taken.

The selected item has been approved

Once the list is refreshed to reflect the changes, we can demonstrate a similar process for bulk actions.

Selecting all records for a bulk action

Once again, a dialog appears so that the rejection reason can be entered.

Entering comments for the selected bulk action

And once the comments have been entered, the action is taken for all selected records.

Bulk action taken on 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.

Empty list after all items have been resolved

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.

var eventNames = {
	referenceClick: 'data_table.referenceClick',
	aggregateClick: 'data_table.aggregateClick',
	buttonClick: 'data_table.buttonClick',
	bulkAction: 'data_table.bulkAction'
};

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.

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;
	}
}

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.

Companion Widgets for SNH Data Tables

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

List of assigned Incidents with current comment status

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.

Ticket Conversations widget pop-up

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.

New comment posted in the Ticket Conversations widget

Once the modal pop-up window is closed, the list should be updated to reflect the new comment status of the incident.

Updated Incident list after providing required comment

Here is the configuration script to produce the above table using the SNH Data Table from JSON Configuration widget.

var MyWorkConfig = Class.create();
MyWorkConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'all',
		label: 'All',
		roles: ''
	}],

	state: [{
		name: 'all',
		label: 'All'
	}],

	table: {
		all: [{
			name: 'incident',
			displayName: 'Incident',
			all: {
				filter: 'active=true^assigned_toDYNAMIC90d1921e5f510100a9ad2572f2b477fe',
				fields: 'number,caller_id,state,opened_at,short_description',
				svcarray: [{
					name: 'comment',
					label: 'Comment',
					heading: '',
					script: 'global.LastCommentValueProvider'
				}],
				aggarray: [],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			}
		}]
	},

	type: 'MyWorkConfig'
});

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.

var eventNames = {
	referenceClick: 'data_table.referenceClick',
	aggregateClick: 'data_table.aggregateClick',
	buttonClick: 'data_table.buttonClick',
	bulkAction: 'data_table.bulkAction'
};

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.

$rootScope.$on(eventNames.buttonClick, function(e, parms) {
	var modelOptions = {
		title: "${Incident Conversation}",
		widget: "widget-ticket-conversation",
		widgetInput: {
			sys_id: parms.sys_id,
			table: 'incident'
		},
		buttons: [
			{label: '${Done}', cancel: true}
		],
		size: 'lg'
	};
	spModal.open(modelOptions).then(function() {
		$window.location.reload();
	}, function() {
		$window.location.reload();
	});
});

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.

Collaboration Store, Part LXXX

“Walk that walk and go forward all the time. Don’t just talk that talk, walk it and go forward. Also, the walk didn’t have to be long strides; baby steps counted too. Go forward.”
Chris Gardner

Last time, we added some code to our storefront to launch the application installation process. Today, we want to build a detail screen to show all of the detailed information for a specific app. Although we could create a new UI Page or Portal Page for that, and have the tile click branch to that page, it seems as if it would be better if we just had a simple modal pop-up screen so that we could remain on the main shopping experience page after closing the modal dialog. To begin, let’s just create a simple Service Portal widget that we can call up using spModal. We can call our widget Application Details and give it an ID of cs-application-details.

We have already gathered up the application data in the main widget, so we could simply pass everything that we have over to the pop-up widget; however, that would make the pop-up widget highly dependent on the main widget, which is something that I try to avoid. I think the better approach will be to pass in the sys_id of the app and let the pop-up widget fetch its own data from the database. For that, we can cut and paste most of the code that we already have in the main widget.

(function() {
	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.logo = appGR.getValue('logo');
			data.record.version = appGR.getDisplayValue('current_version');
			data.record.provider = appGR.getDisplayValue('provider.name');
			data.record.providerLogo = appGR.provider.getRefRecord().getValue('logo');
			data.record.local = appGR.getDisplayValue('provider.instance') == gs.getProperty('instance_name');
			data.record.state = 0;
			if (appGR.getValue('application')) {
				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);
			}
		}
	}

	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;
	}
})();

Before we get too excited about formatting all of this data for display, let’s just throw a single value onto the display and see if we can get the mechanics of bringing up the widget all working. Here is some simple HTML to get things started.

<div>
  <h3>{{c.data.record.name}}</h3>
</div>

That should be enough to get things going, so let’s pop back over to the main widget and see if we can set up a function in the Client script to call up this widget.

$scope.openApplicationModal = function(evt, item) {
	var modelOptions = {
		title: "${Application Details}",
		widget: "cs-application-details",
		widgetInput: {
			sys_id: item.sys_id
		},
		buttons: [],
		footerStyle: {
			display: 'none'
		},
		size: 'lg'
	};
	$scope.applicationModal = spModal.open(modelOptions);
};

Now that we have a function to call, we need to go into the HTML and set up the call to the function when the operator clicks on the tile.

<a href="javascript:void(0);" ng-click="openApplicationModal($event, item)" class="panel-body block height-100" sn-focus="{{::item.highlight}}" aria-labelledby="cs_app_{{::item.sys_id}}" aria-describedby="cs_app_desc_{{::item.sys_id}}">
  <div>
    <h3 class="h4 m-t-none m-b-xs text-overflow-ellipsis" title="{{::item.name}}" style="padding-bottom:1px" id="cs_app_{{::item.sys_id}}">{{::item.name}}</h3>
    <img ng-src="{{::item.logo}}.iix?t=small" ng-if="item.logo" alt="" class="m-r-sm m-b-sm item-image pull-left" aria-hidden="true"/>
    <div class="text-muted item-short-desc catalog-text-wrap" id="cs_app_desc_{{::item.sys_id}}">{{::item.description}}</div>
  </div>
</a>

That should be enough to be able to open up the store and give things the old college try.

Simple application details pop-up

Not bad. OK, now that we have the basic mechanics working, we need to design the layout of the pop-up and also add whatever functionality we might want such as links to any forms or actions. At this point in the process, we have not added that much in the way of extra data. There are no categories or keywords or comments or user ratings or statistics or much of anything else in the way of interesting information outside of the name and description of the application. Some or all of that may come in some future version, but for now, about the best we can do to add detail would be to add the version history and to throw in a few useful links.

Store application detail pop-up

The above example is for an app pulled down from the store. For local apps that have been pushed up to the store, the look would be similar, but with a few differences.

Local application detail pop-up

For the local application, we should probably continue with the modified background, just to be consistent, but you get the idea. To pull this off, we will have to fetch more data from the database, and work out all of the associated HTML. Once that is done, that should be good enough to push out a new version so that folks can try it all out at home. That’s still a bit of work, so let’s deal with all of that next time out.

Content Selector Configuration Editor, Part IX

“Plans are only good intentions unless they immediately degenerate into hard work.”
Peter Drucker

It seemed as if we had this topic all wrapped up a while back, but then we had to play around with the Buttons and Icons. That should have been the end of that, but then we went and added Bulk Actions to the hacked up Data Table widget, which has now broken our new Content Selector Configuration Editor. So now we have to add support for the Bulk Action configuration to the editor to put things back together again. Fortunately, this is all very similar stuff, so it is mainly just a matter of making some copies of things that we have already built and then hacking them up just a bit to fit the current need.

To start with, we will need a Bulk Action Editor pop-up very similar to all of the other pop-up editors that we have been creating, and since Bulk Actions only have a name and a label for properties, the State Editor seems like the best choice for something to copy, since it too only has a name and a label for properties. Once we pull that widget up on the widget form, change the name, and then select Insert and Stay from the context menu, we can start working on our new copy. There is literally nothing to change with the HTML:

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="persp"
      snh-label="Name"
      snh-required="true"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

… and only a function name change on the client script:

function BulkActionEditor($scope, $timeout) {
	var c = this;

	$scope.cancel = function() {
		$timeout(function() {
			angular.element('[ng-click*="buttonClicked"]').get(0).click(); 
		});
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			$timeout(function() {
				angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	$timeout(function() {
		angular.element('[class*="modal-footer"]').css({display:'none'});
	}, 100);
}

As you may recall, there is no server-side code for these guys, so that’s all there is to that. Now we have a Bulk Action Editor. There is a little more work involved in the main Content Selector Configurator widget. On the HTML, we need to define a table for the Bulk Actions:

<div id="label.actarray" class="snh-label" nowrap="true">
  <label for="actarray" class="col-xs-12 col-md-4 col-lg-6 control-label">
    <span id="status.actarray"></span>
    <span title="Bulk Actions" data-original-title="Bulk Actions">${Bulk Actions}</span>
  </label>
</div>
<table class="table table-hover table-condensed">
  <thead>
    <tr>
      <th style="text-align: center;">${Label}</th>
      <th style="text-align: center;">${Name}</th>
      <th style="text-align: center;">${Edit}</th>
      <th style="text-align: center;">${Delete}</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="act in tbl[state.name].actarray" ng-hide="btn.removed">
      <td data-th="${Label}">{{act.label}}</td>
      <td data-th="${Name}">{{act.name}}</td>
      <td data-th="${Edit}" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editAction(act)" alt="Click here to edit this Bulk Action" title="Click here to edit this Bulk Action" style="cursor: pointer;"/></td>
      <td data-th="${Delete}" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteAction(act, tbl[state.name].actarray)" alt="Click here to delete this Bulk Action" title="Click here to delete this Bulk Action" style="cursor: pointer;"/></td>
    </tr>
  </tbody>
</table>
<div style="width: 100%; text-align: right;">
  <action ng-click="editAction('new', tbl[state.name].actarray, tbl);" class="btn btn-primary ng-binding ng-scope" role="action" title="Click here to add a new Bulk Action">Add a new Bulk Action</action>
</div>

On the client side, we need to add a couple more functions to handle the editing and deleting of the actions, which we can basically copy from the same functions we already have for editing and deleting buttons and icons.

$scope.editAction = function(action, actArray) {
	var shared = {page_id: {value: '', displayValue: ''}};
	if (action != 'new') {
		shared.label = action.label;
		shared.name = action.name;
	}
	spModal.open({
		title: 'Bulk Action Editor',
		widget: 'bulk-action-editor',
		shared: shared
	}).then(function() {
		if (action == 'new') {
			action = {};
			actArray.push(action);
		}
		action.label = shared.label || '';
		action.name = shared.name || '';
	});
};

$scope.deleteAction = function(action, actArray) {
	var confirmMsg = '<b>Delete Bulk Action</b>';
	confirmMsg += '<br/>Are you sure you want to delete the ' + action.label + ' Bulk Action?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			var a = -1;
			for (var b=0; b<actArray.length; b++) {
				if (actArray[b].name == action.name) {
					a = b;
				}
			}
			actArray.splice(a, 1);
		}
	});
};

On the server side, we just need to add some code to format the Bulk Action section of the table configurations, which we can copy from the code that formats the Buttons/Icons section, and then delete all of the extra properties that are not needed for Bulk Actions.

script += ",\n				actarray: [";
lastSeparator = '';
for (var a=0; a<tableTable[tableState.name].actarray.length; a++) {
	var thisAction = tableTable[tableState.name].actarray[a];
	script += lastSeparator;
	script += "{\n					name: '";
	script += thisAction.name;
	script += "',\n					label: '";
	script += thisAction.label;
	script += "'\n				}";
	lastSeparator = ",";
}
script += "]";

Now all we need to do is pull up our modified ButtonTestConfig script in the editor and see how we did.

Content Selector Configuration Editor with new Bulk Actions section

So far, so good. Now let’s pull up that new Bulk Action Editor and see how that guy works:

Bulk Action Editor

Not bad. A little bit of testing here and there, just to make sure that we didn’t break anything along the way, and we should be good to go. Now if we can just stop tinkering with things, this Update Set should be the final release and we can move on to other exciting adventures.

Content Selector Configuration Editor, Part VIII

“Tinkering is something we need to know how to do in order to keep something like the space station running. I am a tinkerer by nature.”
Leroy Chiao

I know what you are thinking: Didn’t we wrap this series up last time with the release of the final Update Set? Well, technically you would be correct in that we finished building all of the parts and bundled them all up in an Update Set for those of you who like to play along at home, but … we really did not spend a whole lot of time on the purpose for all of this, so I thought it might be a good time to back up the truck just a tad and demo some of the features of the customized data table widget and the associated content selector widget. Mainly, I want to talk about the buttons and icons and how all of that works, but before we do that, let’s go all the way back and talk about the basic idea behind these customizations of some pretty cool stock products.

I really liked the stock Data Table widget that comes bundled with the Service Portal, but the one thing that annoyed me was that each row was one huge clickable link, which was a departure from the primary UI, where every column could potentially be a link if it contained a Reference field. So, I rewired it. Of course, once you start playing with something then you end up doing all kinds of other crazy things and before you know it, you have this whole subset of trinkets and doodads that only you know anything about. So, on occasion, I feel the need to stop talking about what I am building and how it is constructed, and spend a little time talking about how to use it and what it is good for. Hence, today’s discussion of buttons and icons on the customized Data Table widget.

Before we get started, though, I do need to confess that in all of the excitement around creating a way to edit the JSON configuration object for the Configurable Data Table Widget Content Selector, I completely forgot about one of the options for handling a button click: opening up a new portal page. When we first introduced the idea of having buttons and icons on the rows of the Data Table widget, we made allowances for adding a page_id property to the button definition, and if that property were valued, we would link to that page on a click; otherwise, we would broadcast the click details. We did not include the page_id property in either the Content Selector Configuration Editor widget or the Button/Icon Editor widget, so let’s correct that oversight right now. First, we will need to add that property to the HTML for the table of Buttons/Icons.

<table class="table table-hover table-condensed">
  <thead>
    <tr>
      <th style="text-align: center;">${Label}</th>
      <th style="text-align: center;">${Name}</th>
      <th style="text-align: center;">${Heading}</th>
      <th style="text-align: center;">${Icon}</th>
      <th style="text-align: center;">${Icon Name}</th>
      <th style="text-align: center;">${Color}</th>
      <th style="text-align: center;">${Hint}</th>
      <th style="text-align: center;">${Page}</th>
      <th style="text-align: center;">${Edit}</th>
      <th style="text-align: center;">${Delete}</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="btn in tbl[state.name].btnarray" ng-hide="btn.removed">
      <td data-th="${Label}">{{btn.label}}</td>
      <td data-th="${Name}">{{btn.name}}</td>
      <td data-th="${Heading}">{{btn.heading}}</td>
      <td data-th="${Icon}" style="text-align: center;">
        <a ng-if="btn.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{btn.color || 'default'}}" title="{{btn.hint}}" data-original-title="{{btn.hint}}">
          <span class="icon icon-{{btn.icon}}" aria-hidden="true"></span>
          <span class="sr-only">{{btn.hint}}</span>
         </a>
       </td>
       <td data-th="${Icon Name}">{{btn.icon}}</td>
       <td data-th="${Color}">{{btn.color}}</td>
       <td data-th="${Hint}">{{btn.hint}}</td>
       <td data-th="${Page}">{{btn.page_id}}</td>
       <td data-th="${Edit}" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editButton(btn)" alt="Click here to edit this Button/Icon" title="Click here to edit this Button/Icon" style="cursor: pointer;"/></td>
       <td data-th="${Delete}" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteButton(btn, tbl[state.name].btnarray)" alt="Click here to delete this Button/Icon" title="Click here to delete this Button/Icon" style="cursor: pointer;"/></td>
     </tr>
   </tbody>
 </table>

… and then we will need to pass it back and forth between the main widget and the pop-up Button/Icon Editor widget:

$scope.editButton = function(button, btnArray) {
	var shared = {page_id: {value: '', displayValue: ''}};
	if (button != 'new') {
		shared.label = button.label;
		shared.name = button.name;
		shared.heading = button.heading;
		shared.icon = button.icon;
		shared.color = button.color;
		shared.hint = button.hint;
		shared.page_id = {value: button.page_id, displayValue: button.page_id};
	}
	spModal.open({
		title: 'Button/Icon Editor',
		widget: 'button-icon-editor',
		shared: shared
	}).then(function() {
		if (button == 'new') {
			button = {};
			btnArray.push(button);
		}
		button.label = shared.label || '';
		button.name = shared.name || '';
		button.heading = shared.heading || '';
		button.icon = shared.icon || '';
		button.color = shared.color || '';
		button.hint = shared.hint || '';
		button.page_id = shared.page_id.value || '';
	});
};

… and then, of course, we need to update the Button/Icon Editor itself by dragging in the page selector form field from the Reference Page Editor widget and adding it to the fields on the Button/Icon Editor form.

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="the_name"
      snh-label="Name"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.heading"
      snh-name="heading"/>
    <snh-form-field
      snh-model="c.widget.options.shared.icon"
      snh-name="icon"
      snh-type="icon"/>
    <snh-form-field
      snh-model="c.widget.options.shared.color"
      snh-name="color"
      snh-type="select"
      snh-choices='[{"label":"default","value":"default"},{"label":"primary","value":"primary"},{"label":"secondary","value":"secondary"},{"label":"success","value":"success"},{"label":"danger","value":"danger"},{"label":"warning","value":"warning"},{"label":"info","value":"info"},{"label":"light","value":"light"},{"label":"dark","value":"dark"},{"label":"muted","value":"muted"},{"label":"white","value":"white"}]'/>
    <snh-form-field
      snh-model="c.widget.options.shared.hint"
      snh-name="hint"/>
    <snh-form-field
      snh-type="reference"
      snh-model="c.widget.options.shared.page_id"
      snh-name="page_id"
      placeholder="Choose a Portal Page"
      table="'sp_page'"
      display-field="'title'"
      display-fields="'id'"
      value-field="'id'"
      search-fields="'id,title'"/>
    <div id="element.example" class="form-group">
      <div id="label.example" class="snh-label" nowrap="true">
        <label for="example" class="col-xs-12 col-md-4 col-lg-6 control-label">
          <span id="status.example"></span>
          <span title="Example" data-original-title="Example">Example</span>
        </label>
      </div>
      <span class="input-group">
        <a ng-if="!c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">{{c.widget.options.shared.label}}</a>
        <a ng-if="c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">
          <span class="icon icon-{{c.widget.options.shared.icon}}" aria-hidden="true"></span>
          <span class="sr-only">{{c.widget.options.shared.hint}}</span>
        </a>
      </span>
    </div>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

That should take care of that little oversight. Now, let’s get back to showing off the various ways in which you can use buttons and icons on the customized Data Table widget.

To start out, let’s use the tool that we just made to create a brand new configuration for a sample page where we can demonstrate how the buttons and icons work. Let’s click on our new Menu Item to bring up the tool, click on the Create a new Content Selector Configuration button, enter the name of our new Script Include, and then click on the OK button.

Creating a new configuration object using the new tool

When our new empty configuration comes up in the tool, let’s define a single Perspective called Button Test, and a couple of different States, Active and Not Active. We want to keep things simple at this point, mainly so as not to distract from our primary purpose here, which is to show how the buttons and icons work. Once we have defined our Perspective and States, click on the Add new Table button and select the Incident table from the list.

Selecting the Incident table from the pop-up selection list

Select just a couple of fields, say Number and Short Description, and set the filter for the Active State to active=true. Then we can start adding Buttons and Icons using the Add new Button/Icon button.

Adding a new Button/Icon

Those of you paying close attention will notice that the image above was taken before we added the page_id property to the Button/Icon configuration. The newer version has a pick list of portal pages below the Hint and above the Example. You will want to define at least one Button/Icon with a page value. Keep adding different buttons and icons until you have a representative sample of different things and then we can see how it all renders out in actual use.

New configuration with several different buttons and icons

Once you have a few set up for testing, save the new configuration and take a look at the resulting script, which should look something like this:

var ButtonTestConfig = Class.create();
ButtonTestConfig.prototype = Object.extendsObject(ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'button_test',
		label: 'Button Test',
		roles: ''
	}],

	state: [{
		name: 'active',
		label: 'Active'
	},{
		name: 'not_active',
		label: 'Not Active'
	}],

	table: {
		button_test: [{
			name: 'incident',
			displayName: 'Incident',
			active: {
				filter: 'active=true',
				fields: 'number,short_description',
				btnarray: [{
					name: 'button1',
					label: 'Button #1',
					heading: '',
					icon: '',
					color: 'info',
					hint: 'This is Button #1',
					page_id: ''
				},{
					name: 'button2',
					label: 'Button #2',
					heading: 'Button #2',
					icon: '',
					color: 'warning',
					hint: 'This is Button #2',
					page_id: ''
				},{
					name: 'button3',
					label: 'Button #3',
					heading: 'B3',
					icon: '',
					color: '',
					hint: 'This is Button #3',
					page_id: 'ticket'
				},{
					name: 'icon1',
					label: 'Icon #1',
					heading: '-',
					icon: 'cross',
					color: 'danger',
					hint: 'Danger Will Robinson!',
					page_id: ''
				},{
					name: 'icon2',
					label: 'Icon #2',
					heading: 'I2',
					icon: 'download',
					color: 'success',
					hint: 'Click here to do something ...',
					page_id: ''
				}],
				refmap: {}
			},
			not_active: {
				filter: 'active=false',
				fields: 'number,short_description',
				btnarray: [],
				refmap: {}
			}
		}]
	},

	type: 'ButtonTestConfig'
});

Now that we have that all ready to rock and roll, we need to configure a new test page so that we can put it to use. From the list of Portal Pages, click on the New button and create a page called button_test and save it so that we can pull it up in the Page Designer.

Setting up the new page in the Page Designer

From the container list, drag over a 3/9 container and drag the Content Selector widget into the narrow portion and the SNH Data Table from URL Configuration widget into the wider portion. You shouldn’t have to edit the Data Table widget, but you will need to click on the pencil icon on the Content Selector widget so that you enter the name of your new configuration script.

Entering the name of the configuration script in the widget options

Once that has been completed, you should be able to pull up your new page in the Service Portal and see how it renders out. You can click on any of your buttons or icons at this point, and if you click on one with a page_id value, that page should come up with the record from that row. If you click on any of the other buttons or icons, through, nothing will happen, because we have not set up anything to react to the button clicks at this point. However, if everything is working as it should be, we are now ready to do just that.

When a button or icon is clicked on the customized Data Table widget, and there is no page_id value defined, all that happens is that the details are broadcast out. Some other widget on the page needs to be listening for that broadcast in order for something to happen. The secret, then, is to have some client-side code somewhere that looks something like this:

$rootScope.$on('button.click', function(e, parms) {
	// do something 
});

For demonstration purposes we can build a simple test widget that will do just that, and in response to a click event, display all of the details of that event in an spModal alert. Let’s call this widget Button Click Handler Example, and give it the following client-side code:

function ButtonClickHandlerExample(spModal, $rootScope) {
	var c = this;

	$rootScope.$on('button.click', function(e, parms) {
		displayClickDetails(parms);		
	});

	function displayClickDetails(parms) {
		var html = '<div>'; 
		html += ' <div class="center"><h3>You clicked on the ' + parms.button.name + ' button</h3></div>\n';
		html += ' <table>\n';
		html += '  <tbody>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Table: &nbsp;</td>\n';
		html += '    <td>' + parms.table + '</td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Sys ID: &nbsp;</td>\n';
		html += '    <td>' + parms.sys_id + '</td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Button: &nbsp;</td>\n';
		html += '    <td><pre>' + JSON.stringify(parms.button, null, 4) + '</pre></td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Record: &nbsp;</td>\n';
		html += '    <td><pre>' + JSON.stringify(parms.record, null, 4) + '</pre></td>\n';
		html += '   </tr>\n';
		html += '  </tbody>\n';
		html += ' </table>\n';
		html += '</div>';
		spModal.alert(html);
	}
}

Now that we have our widget, we can pop back into the Page Designer and drag the widget anywhere on the screen. It has no displayable content, so it really doesn’t matter where you put it as long as it is present on the page somewhere. Once that’s done, we can pull up our page on the portal again and start clicking around again to see what we get.

Button click results

In addition to the details of the record on that row and the button that was clicked, you also get the name of the table and the sys_id of the record. Any of this data can be used to perform any number of potential actions, all of which are completely outside of the two reusable components, the customized Data Table and the configurable Content Selector. You shouldn’t have to modify either of those widgets to create custom functionality; just configure the Content Selector with an appropriate JSON config object, and then add any custom click handlers to your page for any button actions that you would like to configure. Here is an Update Set that includes the page_id corrections as well as the button test widget so that you can click around on your own and see how everything plays together. There is an even better version here.

Content Selector Configuration Editor, Part VI

“It is amazing what you can accomplish if you do not care who gets the credit.”
Harry Truman

At the end of our last installment in this series we had coded all of the client-side functions to add and remove Buttons/Icons and Reference Pages, but still needed to create the modal widgets needed to edit the values for those. The Button/Icon editor is the more complex of the two, plus it’s the first one that we encounter on the page, so let’s start with that one, and as usual, let’s start with the HTML.

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="the_name"
      snh-label="Name"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.heading"
      snh-name="heading"/>
    <snh-form-field
      snh-model="c.widget.options.shared.icon"
      snh-name="icon"
      snh-type="icon"/>
    <snh-form-field
      snh-model="c.widget.options.shared.color"
      snh-name="color"
      snh-type="select"
      snh-choices='[
        {"label":"default","value":"default"},
        {"label":"primary","value":"primary"},
        {"label":"secondary","value":"secondary"},
        {"label":"success","value":"success"},
        {"label":"danger","value":"danger"},
        {"label":"warning","value":"warning"},
        {"label":"info","value":"info"},
        {"label":"light","value":"light"},
        {"label":"dark","value":"dark"},
        {"label":"muted","value":"muted"},
        {"label":"white","value":"white"}]'/>
    <snh-form-field
      snh-model="c.widget.options.shared.hint"
      snh-name="hint"/>
    <div id="element.example" class="form-group">
      <div id="label.example" class="snh-label" nowrap="true">
        <label for="example" class="col-xs-12 col-md-4 col-lg-6 control-label">
          <span id="status.example"></span>
          <span title="Example" data-original-title="Example">Example</span>
        </label>
      </div>
      <span class="input-group">
        <a ng-if="!c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">{{c.widget.options.shared.label}}</a>
        <a ng-if="c.widget.options.shared.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{c.widget.options.shared.color || 'default'}}" title="{{c.widget.options.shared.hint}}" data-original-title="{{c.widget.options.shared.hint}}">
          <span class="icon icon-{{c.widget.options.shared.icon}}" aria-hidden="true"></span>
          <span class="sr-only">{{c.widget.options.shared.hint}}</span>
        </a>
      </span>
    </div>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

Pretty standard stuff. There are just a few more fields here than with some of the other things that we have been editing in a pop-up window, and one of them happens to be the new icon field type. Also, there is an “Example” field to show you what the Button/Icon will look like based on the values that you have entered. Here’s how it looks in action:

Button/Icon Editor

Other than the additional fields, it’s pretty much the same as the others, and the client-side code is virtually identical to what we have used in the past, with the one exception of having to assign spModal to the $scope so that it can be available to the Icon Picker.

function ButtonIconEditor($scope, $timeout, spModal) {
	var c = this;

	$scope.spModal = spModal;

	$scope.selectIcon = function() {
		spModal.open({
			title: 'Select Icon',
			widget: 'icon-picker',
			buttons: [
				{label: '${Cancel}', cancel: true}
			],
			size: 'sm',
		}).then(function(response) {
			c.widget.options.shared.icon = response.selected;
		});
	};

	$scope.cancel = function() {
		$timeout(function() {
			angular.element('[ng-click*="buttonClicked"]').get(0).click(); 
		});
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			$timeout(function() {
				angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	$timeout(function() {
		angular.element('[class*="modal-footer"]').css({display:'none'});
	}, 100);
}

As with our other pop-up editor widgets, there is no server-side code, so that’s the entire widget. The Reference Page editor is even simpler.

<div>
  <form name="form1">
    <snh-form-field
      snh-type="reference"
      snh-model="c.widget.options.shared.table"
      snh-name="table"
      snh-change="buildFieldFilter();"
      snh-required="true"
      placeholder="Choose a Table"
      table="'sys_db_object'"
      display-field="'label'"
      display-fields="'name'"
      value-field="'name'"
      search-fields="'name,label'"/>
    <snh-form-field
      snh-type="reference"
      snh-model="c.widget.options.shared.page"
      snh-name="page"
      snh-required="true"
      placeholder="Choose a Portal Page"
      table="'sp_page'"
      display-field="'title'"
      display-fields="'id'"
      value-field="'id'"
      search-fields="'id,title'"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

Here, we just have two sn-record-pickers, one for the reference table and the other for the portal page that you want to bring up whenever someone clicks on a reference value from that table. And once again, the client-side code looks very familiar.

function ReferencePageEditor($scope, $timeout) {
	var c = this;

	$scope.cancel = function() {
		$timeout(function() {
			angular.element('[ng-click*="buttonClicked"]').get(0).click(); 
		});
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			$timeout(function() {
				angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	$timeout(function() {
		angular.element('[class*="modal-footer"]').css({display:'none'});
	}, 100);
}

That takes care of everything that we discussed last time, but last time we neglected to make allowances for a couple of very import features: the ability to add a new Table and the ability to remove a Table. Since each Perspective has its own list of Tables, we will need to add a New Table button at the bottom of the list for each Perspective. To remove a Table, we can just add a Delete icon next to the Table name for that purpose. That will change the basic HTML structure to now look like this:

<div>
  <h4 class="text-primary">${Tables}</h4>
</div>
<uib-tabset active="active">
  <uib-tab ng-repeat="persp in c.data.config.perspective track by persp.name" heading="{{persp.label}}">
    <div ng-repeat="tbl in c.data.config.table[persp.name] track by tbl.name" style="padding-left: 25px;">
      <h4 style="color: darkblue">
        {{tbl.displayName}}
        ({{tbl.name}})
        <img src="/images/delete_row.gif" ng-click="deleteTable(persp.name, tbl.name)" alt="Click here to permanently delete table {{tbl.name}} from this Perspective" title="Click here to permanently delete table {{tbl.name}} from this Perspective" style="cursor: pointer;"/>
      </h4>
      <uib-tabset active="active">
        <uib-tab ng-repeat="state in c.data.config.state track by state.name" heading="{{state.label}}">
          <div style="padding-left: 25px;">
 
<!-----  all of the specific properties are defined here  ----->
 
          </div>
        </uib-tab>
      </uib-tabset>
    </div>
    <div style="width: 100%;">
      <button ng-click="newTable(persp.name)" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to add a new table to the {{persp.label}} perspective">Add a new Table</button>
    </div>
  </uib-tab>
</uib-tabset>

And of course, we will need functions to handle both the Table Add and Table Remove processes. For the Add, let’s use a pop-up Table selector that will allow you to select a Table from a list. For the Delete, we can adapt one of the other Delete functions that we have already written.

$scope.deleteTable = function(perspective, tableName) {
	var confirmMsg = '<b>Delete Table</b>';
	confirmMsg += '<br/>Deleting the ';
	confirmMsg += tableName;
	confirmMsg += ' Table will also delete all information for that Table in every State in this Perspective.';
	confirmMsg += '<br/>Are you sure you want to delete this Table?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			var a = -1;
			for (var b=0; b<c.data.config.table[perspective].length; b++) {
				if (c.data.config.table[perspective][b].name == tableName) {
					a = b;
				}
			}
			c.data.config.table[perspective].splice(a, 1);
		}
	});
};

The function to pop open the modal Table Selector widget is quite familiar as well.

$scope.newTable = function(perspective) {
	var shared = {};
	spModal.open({
		title: 'Table Selector',
		widget: 'table-selector',
		shared: shared
	}).then(function() {
		c.data.config.table[perspective].push(shared);
	});
};

… and the widget itself we can clone from the Reference Page widget, which already had a Table picker defined.

<div>
  <form name="form1">
    <snh-form-field
      snh-type="reference"
      snh-model="c.data.table"
      snh-name="table"
      snh-change="tableSelected();"
      snh-required="true"
      snh-help="Choose a Table to be added to this Perspective"
      placeholder="Choose a Table"
      table="'sys_db_object'"
      display-field="'label'"
      display-fields="'name'"
      value-field="'name'"
      search-fields="'name,label'"/>
  </form>
</div>

Since all we want them to do is to select a table, we can use an snh-change to call the function and send back the selection.

function TableSelector($scope, $timeout) {
	var c = this;

	$scope.tableSelected = function() {
		if (c.data.table.value > '') {
			c.server.update().then(function(response) {
				c.widget.options.shared.name = response.table.value;
				c.widget.options.shared.displayName = response.table.displayValue;
				$timeout(function() {
					angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
				});
			});
		}
	};
}

On server side, we just make sure that we have both a table name and a display name.

(function() {
	data.table = {};

	if (input && input.table && input.table.value) {
		data.table = input.table;
		if (!data.table.displayValue) {
			data.table.displayValue = getItemName(data.table.value);
		}
	}

	function getItemName(key) {
		var ciGR = new GlideRecord(key);
		return ciGR.getLabel();
	}
})();

That pretty much wraps up all of the editing functions. We still need to throw in some code to save all of the changes, but that might get pretty involved, so let’s say we save that for our next installment.

Content Selector Configuration Editor, Part V

“It takes considerable knowledge just to realize the extent of your own ignorance.”
Thomas Sowell

At the end of our last installment in this series, we had the HTML for the Tables section, but none of the underlying code to make it all work. Now it’s time to create that code by building all of the client-side functions needed to support all of the ng-click attributes sprinkled throughout the HTML. The first one that you come to in this section is the editButton function that passes the button object as an argument. This should pop open a modal editor just like we did with the Perspectives and the States, so this function could be loosely modeled after those other two. Something like this should do the trick:

$scope.editButton = function(button, btnArray) {
	var shared = {};
	if (button != 'new') {
		shared.label = button.label;
		shared.name = button.name;
		shared.heading = button.heading;
		shared.icon = button.icon;
		shared.color = button.color;
		shared.hint = button.hint;
	}
	spModal.open({
		title: 'Button/Icon Editor',
		widget: 'button-icon-editor',
		shared: shared
	}).then(function() {
		if (button == 'new') {
			button = {};
			btnArray.push(button);
		}
		button.label = shared.label;
		button.name = shared.name;
		button.heading = shared.heading;
		button.icon = shared.icon;
		button.color = shared.color;
		button.hint = shared.hint;
	});
};

We had to add a second argument to the function in this case, but only when creating a new Button/Icon. For an existing object, it is already in place in the object tree, but in the case of a new Button/Icon that we create from a new blank object, we need to know where to put it so that it lives with all of its siblings. Other than that one little wrinkle, the rest is pretty much the same as we have built before. The deleteButton function is also a little different in that we do not have a specified index, so we have to spin through the list to determine the index. Once we have that, though, all we need to do is remove the Button/Icon from the list, as there are no other dependent objects that have to be removed, unlike the Perspectives and the States.

$scope.deleteButton = function(button, btnArray) {
	var confirmMsg = '<b>Delete Button/Icon</b>';
	confirmMsg += '<br/>Are you sure you want to delete the ' + button.label + ' Button/Icon?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			var a = -1;
			for (var b=0; b<btnArray.length; b++) {
				if (btnArray[b].name == button.name) {
					a = b;
				}
			}
			btnArray.splice(a, 1);
		}
	});
};

That takes care of the Buttons/Icons … now we have to do essentially the same thing for the Reference Pages. Once again, we have a few little wrinkles that make things not quite exactly the same. For one thing, the Button/Icon data is stored in an Array, but the Reference Page data is stored in a Map. Also, both of the properties for a Reference Page (Table and Portal Page) have values that come from a ServiceNow database table, so the pop-up editor can use an sn-record-picker for both fields. That means their values will be stored in objects, not strings, so again, not an exact copy of the other functions. Still, it looks pretty close to all of its cousins:

$scope.editRefMap = function(table, refMap) {
	var shared = {table: {}, page: {}};
	if (table != 'new') {
		shared.table.value = table;
		shared.table.displayValue = table;
		shared.page.value = refMap[table];
		shared.page.displayValue = refMap[table];
	}
	spModal.open({
		title: 'Reference Page Editor',
		widget: 'reference-page-editor',
		shared: shared
	}).then(function() {
		if (table != 'new') {
			if (shared.table.value != table) {
				delete refMap[table];
			}
		}
		refMap[shared.table.value] = shared.page.value;
	});
};

… and because of those differences, the deleteRefMap function turns out to be the most simplest of all:

$scope.deleteRefMap = function(table, refMap) {
		var confirmMsg = '<b>Delete Reference Page</b>';
		confirmMsg += '<br/>Are you sure you want to delete the ' + table + ' table reference page mapping?';
		spModal.confirm(confirmMsg).then(function(confirmed) {
			if (confirmed) {
				delete refMap[table];
			}
		});
	};

So now we are done with all of the client-side functions, but our work is still not finished. Both of the edit functions pop open modal editor widgets, so we are going to need to build those. We already have a couple of different models created for the Perspective and State editors, so it won’t be as if we have to start out with a blank canvas. Still, there is a certain amount of work involved, so that sounds like a good place to start out in our next installment.

Yet Another Service Portal Form Field Type

“In every kind of endeavor, there are ample opportunities for extra effort. Grab those opportunities, embrace that extra effort and transform ordinary mediocrity into bright and shining excellence.”
Ralph Marston

Just when I start thinking that I have finally come up with all of the different Form Field Types than I could possibly use, something comes along to make me realize that I could maybe use just one or two more. While building the Button/Icon Editor widget for my Content Selector Configuration Editor, I came across a need for a field that would allow me to select an Icon. I already had a pop-up Icon selection widget, but there wasn’t a form field type that would allow me to configure that as a selection source. I thought about just coding the HTML myself and not bothering with trying to use the snh-form-field element, but then it occurred to me that if I was going to go to all that trouble, I might as well make the changes to the form field provider and release a new version. The work effort seemed to be roughly equivalent, so here we go.

To begin, we need to add the new type to the list of supported field types up at the top. We will call our new field type icon, and just add it to the existing list in its place in alphabetical order.

var SUPPORTED_TYPE = ['checkbox', 'choicelist', 'date', 'datetime-local', 'email', 'feedback', 'icon', 'inlinemultibox', 'inlineradio', 'mention', 'month', 'multibox', 'number', 'password', 'radio', 'rating', 'reference', 'select', 'tel', 'text', 'textarea', 'time', 'url', 'week'];

My plan is to make the input field read-only and only accept input from the pop-up icon picker. To launch the icon picker, I plan to put a search icon at the end of the input element, much like the icons for the url, email, and tel field types. In fact, the code will be so similar, I think I will add the type to the list of special types, which will drive the processing into the section that builds the layout for those types. Currently, that code looks like this:

var SPECIAL_TYPE = {
	email: {
		title: 'Send an email to {{MODEL}}',
		href: 'mailto:{{MODEL}}',
		icon: 'mail'
	},
	tel: {
		title: 'Call {{MODEL}}',
		href: 'tel:{{MODEL}}',
		icon: 'phone'
	},
	url: {
		title: 'Open {{MODEL}} in a new browser window',
		href: '{{MODEL}}" target="_blank',
		icon: 'pop-out'
	}
};

In addition to adding the icon type to the list, I am also going to add an additional property to each type configuration to enable conditional application of the read-only option. For the new icon type, that value will be set to ‘ readonly=”readonly”‘, and for all other types, it will just be an empty string. This way, I can insert the type-specific value into the input element definition without having to have any conditional logic in that part of the process. The updated version of the SPECIAL_TYPE variable value now looks like this:

var SPECIAL_TYPE = {
	email: {
		title: 'Send an email to {{MODEL}}',
		href: 'mailto:{{MODEL}}',
		icon: 'mail',
		extra: ''
	},
	icon: {
		title: 'Select an Icon',
		href: 'javascript:void(0)" ng-click="selectIcon(\'MODEL\');',
		icon: 'search',
		extra: ' readonly="readonly"'
	},
	tel: {
		title: 'Call {{MODEL}}',
		href: 'tel:{{MODEL}}',
		icon: 'phone',
		extra: ''
	},
	url: {
		title: 'Open {{MODEL}} in a new browser window',
		href: '{{MODEL}}" target="_blank',
		icon: 'pop-out',
		extra: ''
	}
};

Those of you paying close attention might also have noticed the other little hack used on the new type: the href value terminates the href attribute and begins a new ng-click attribute. We don’t want to link to another page in this instance, so the value will set the href attribute to “javascript:void(0)” and then add an ng-click attribute to call a new function that will launch the icon picker. We’ll have to add that new function to the scope down in the link section of the provider a little later on.

With that now in place, we can scroll down to the buildSpecialTypes function and start hacking that up to accommodate our new type. Right at the top we grab all of the values relevant to the type for which we are currently building, That code looks like this:

var title = SPECIAL_TYPE[type].title.replace('MODEL', model);
var href = SPECIAL_TYPE[type].href.replace('MODEL', model);
var icon = SPECIAL_TYPE[type].icon;

Since we added an additional property to the type configurations, we will want to modify that code to include that new property as well:

var title = SPECIAL_TYPE[type].title.replace('MODEL', model);
var href = SPECIAL_TYPE[type].href.replace('MODEL', model);
var icon = SPECIAL_TYPE[type].icon;
var extra = SPECIAL_TYPE[type].extra;

Now all we need to do is to use that new value in the input element definition that we are building. That’s just a a few lines down and looks like this:

htmlText += "       <input class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' ng-required="' + required + '"':'') + "/>\n";

Let’s just add the potential readonly attribute at the very end:

htmlText += "       <input class=\"snh-form-control\" ng-model=\"" + model + "\" id=\"" + name + "\" name=\"" + name + "\" type=\"" + type + "\"" + passThroughAttributes(attrs) + (required?' ng-required="' + required + '"':'') + extra + "/>\n";

Well, that was easy! We still don’t have any code to pop up the icon selector just yet, but we’ve done enough to be able to see how it looks, and to see if we have broken anything so far. Let’s add a new field of type icon to our existing form field test page and see how it comes out. In its simplest form, it would look something like this:

<snh-form-field
  snh-model="c.data.icon"
  snh-name="icon"
  snh-type="icon"/>

Now let’s take a quick peek.

First visual test of the new icon field type

Well, it is definitely read-only, but we did not get our icon displayed at the end of the line as I was hoping. It seems that I neglected to take into account the logic that suppresses the icon if there is no valid data in the field. We don’t really need that in this circumstance, and in fact, we absolutely don’t want it, so we need to make another little modification. This was all one line before, but now we only want to insert the part that hides the icon if the field type is not icon.

htmlText += "       <span class=\"input-group-btn\"";
if (type != 'icon') {
	htmlText += " ng-show=\"" + fullName + ".$valid && " + model + " > ''\"";
}
htmlText += ">\n";

Now, let’s take a look.

Second visual test of the new icon field type

Now that’s better. Now we need to add the function that needs to be called whenever someone clicks on the icon. That goes all the way down at the bottom of the provider script.

scope.selectIcon = function(model) {
	scope.spModal.open({
		title: 'Select Icon',
		widget: 'icon-picker',
		buttons: [
			{label: '${Cancel}', cancel: true}
		],
		size: 'sm',
	}).then(function(response) {
		scope.$eval(model + " = '" + response.selected + "';");
	});
};

Since we don’t have any way to pull in spModal at this point, we are going to have to rely on the client script in the widget to include spModal in their function arguments and attach spModal to the $scope. We already did something similar with snMention, so this will be essentially a copy of that same approach. Here it is on the form field test page, where we have both:

function FormFieldTest($scope, snMention, spModal) {
	var c = this;
	$scope.snMention = snMention;
	$scope.spModal = spModal;
	c.data.picker = {};
}

Now we can take one more look at our test page to see how things will work:

First functional test of the new icon field type

Sweet! Clicking on the search icon pops up the icon picker and clicking on one of the icons on the list populates the form field and closes the picker. And now we have yet one more type of form field. Just like that. I’ll do a little more testing, just to make sure that we did not miss anything, and then I will round up all of the parts and pieces and put them into a new Update Set.

Content Selector Configuration Editor, Part III

“For every complex problem there is an answer that is clear, simple, and wrong.”
H. L. Mencken

Now that we have completed all of the coding on the main widget, it is time to build the new Perspective Editor widget that we will launch in the modal dialog. This will be a simple form with three input fields for the three properties of a Perspective: Label, Name, and Roles. The first two will be simple text fields, but we can select the Roles from a list, so for that we can leverage our old friend, the sn-record-picker. Once again, we can start out with the HTML, just to see how things look.

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="persp"
      snh-label="Name"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.roles"
      snh-name="roles"
      snh-type="reference"
      table="'sys_user_role'"
      field="c.widget.options.shared.roles"
      default-query="'active=true'"
      display-field="'name'"
      search-fields="'name'"
      value-field="'name'"
      multiple="true"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

Basically, this is just three standard snh-form-field elements, the first two being of type text and the last one being of type reference, which is just a wrapper around the sn-record-picker. There are a couple of things to note here: 1) when I tried to use the word name for the name of the name field, it crashed the widget, so I called it persp instead, and 2) I ended up adding a couple of buttons to the layout, even though the spModal already provides buttons for you (more on that later). I’m not sure why using the word name crashed the widget, but I have a sinking feeling that there is some kind of bug in the snh-form-field code. I didn’t really feel like digging into that right at the moment, though, and changing the name fixed the problem, so I’m good for now. Here’s how it looks when it gets launched:

Perspective Editor layout

Now, about those buttons: if you don’t override the defaults, an spModal pop-up will have two standard buttons, Cancel and OK. I wanted to use those buttons, but I also wanted to validate the form, and I’m not smart enough to know how to insert form validation underneath the OK button so that it won’t just go right back to the main page and close the pop-up. I tried a few things, but nothing worked, so I ended up adding my own buttons, hiding the originals, but still clicking on them programmatically to obtain their original function. It’s pretty much a convoluted hack, but it gets the job done, so I’ll take it. Here is the client-side code that makes all of that work:

function PerspectiveEditor($scope, $timeout) {
	var c = this;

	$scope.cancel = function() {
		$timeout(function() {
			angular.element('[ng-click*="buttonClicked"]').get(0).click(); 
		});
	};

	$scope.save = function() {
		if ($scope.form1.$valid) {
			$timeout(function() {
				angular.element('[ng-click*="buttonClicked"]').get(1).click(); 
			});
		} else {
			$scope.form1.$setSubmitted(true);
		}
	};

	$timeout(function() {
		angular.element('[class*="modal-footer"]').css({display:'none'});
	}, 100);
}

That’s it. There is no server-side code on this one and no link code, so that’s the entire widget. That completes everything for the Perspectives section, and now that it all checks out, we can pretty much just copy it all to create the States section. Let’s start with the HTML:

<div>
  <h4 class="text-primary">${States}</h4>
</div>
<div>
  <table class="table table-hover table-condensed">
    <thead>
      <tr>
        <th style="text-align: center;">Label</th>
        <th style="text-align: center;">Name</th>
        <th style="text-align: center;">Edit</th>
        <th style="text-align: center;">Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in c.data.config.state" ng-hide="item.removed">
        <td data-th="Name">{{item.label}}</td>
        <td data-th="Label">{{item.name}}</td>
        <td data-th="Edit" style="text-align: center;"><img src="/images/edittsk_tsk.gif" ng-click="editState($index)" alt="Click here to edit the details of this State" title="Click here to edit the details of this State" style="cursor: pointer;"/></td>
        <td data-th="Delete" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="deleteState($index)" alt="Click here to permanently delete this State" title="Click here to permanently delete this State" style="cursor: pointer;"/></td>
      </tr>
    </tbody>
  </table>
</div>
<div style="width: 100%; text-align: right;">
  <button ng-click="editState('new')" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to add a new State">Add a new State</button>
</div>

This is just a line by line copy of the Perspectives section with the Roles column removed and the names changed. Let’s see how it looks.

States section added to widget

It looks good, and it has been pretty easy so far. The client-side functions should also be quite similar, although removing and altering States in the Table section of the JSON object will be a little more involved, as a State object appears in every Table in every Perspective. We’ll have to throw in a couple of nested loops to deal with every one of those when things change. Other than that, though, everything else should be a wholesale copy of the same code used in the Perspective section. Here are the new client-side functions:

$scope.editState = function(i) {
	var shared = {};
	if (i != 'new') {
		shared.label = c.data.config.state[i].label;
		shared.name = c.data.config.state[i].name;
	}
	spModal.open({
		title: 'State Editor',
		widget: 'e4bdae0d2f3b60104425fcecf699b649',
		shared: shared
	}).then(function() {
		if (i == 'new') {
			for (var x1 in c.data.config.perspective) {
				var p1 = c.data.config.perspective[x1].name;
				for (var y1 in c.data.config.table[p1]) {
					var table1 = c.data.config.table[p1][y1];
					table1[shared.name] = {btnarray: [], refmap: {}};
				}
			}
			i = c.data.config.state.length;
			c.data.config.state.push({});
		} else {
			if (shared.name != c.data.config.state[i].name) {
				for (var x2 in c.data.config.perspective) {
					var p2 = c.data.config.perspective[x2].name;
					for (var y2 in c.data.config.table[p2]) {
						var table2 = c.data.config.table[p2][y2];
						table2[shared.name] = table2[c.data.config.state[i].name];
						table2[c.data.config.state[i].name] = null;
					}
				}
			}
		}
		c.data.config.state[i].name = shared.name;
		c.data.config.state[i].label = shared.label;
	});
};

$scope.deleteState = function(i) {
	var confirmMsg = '<b>Delete State</b>';
	confirmMsg += '<br/>Deleting the ';
	confirmMsg += c.data.config.perspective[i].label;
	confirmMsg += ' State will also delete all information for that State in every Table in every Perspective.';
	confirmMsg += '<br/>Are you sure you want to delete this State?';
	spModal.confirm(confirmMsg).then(function(confirmed) {
		if (confirmed) {
			for (var x3 in c.data.config.perspective) {
				var p3 = c.data.config.perspective[x3].name;
				for (var y3 in c.data.config.table[p3]) {
					var table3 = c.data.config.table[p3][y3];
					table3[c.data.config.state[i].name] = null;
				}
			}
			c.data.config.state.splice(i, 1);
		}
	});
};

Other than the loops through the Perspectives and Tables and the missing Roles, it’s pretty much the exact same code that we had for the previous section. Now all we need to do is clone the Perspective Editor to create the State Editor, and this section should be completed as well. Dropping the Roles and changing the names leaves the HTML for the State Editor looking like this:

<div>
  <form name="form1">
    <snh-form-field
      snh-model="c.widget.options.shared.label"
      snh-name="label"
      snh-required="true"/>
    <snh-form-field
      snh-model="c.widget.options.shared.name"
      snh-name="persp"
      snh-label="Name"
      snh-required="true"/>
  </form>
  <div style="width: 100%; padding: 5px 50px; text-align: right;">
    <button ng-click="cancel()" class="btn btn-default ng-binding ng-scope" role="button" title="Click here to cancel this edit">Cancel</button>
    &nbsp;
    <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
  </div>
</div>

There is virtually no change at all to the client-side code, and there isn’t any server-side code, so we’re done. That was easy! Here is what it looks like in action:

The new State Editor modal dialog

So now we have completed two of our three sections, which makes it sound like we are two-thirds of the way there, but the next section is quite a bit more complicated than the first two. Tackling that guy sounds like a good place to start next time out.