…thoughts on ServiceNow and digital transformation

Post

Modifying the ServiceNow Microsoft Outlook Plugin to Create a Record in Another Table


The ServiceNow Microsoft Outlook Plugin for ITSM allows for creating incidents and VTB items from email. This is handy for when requestors send incidents directly to an agent instead of “putting in a ticket.” However, out of the box, this plugin only works with the incident table (and VTB). In addition, it annoyingly adds the body of the email to the comments field in the incident. This article will look at adding a menu item to the plugin which creates a story in the rm_story table, putting the body of the email in the description field instead of as a comment.

High level overview:

  1. Activate the ServiceNow Add-in for Microsoft Outlook.
  2. Create a record in the sn_office_control table.
  3. Create a view called “Outlook” on the rm_story table.
  4. Clone and modify the Create Incident portal widget.
  5. Copy and modify the Outlook Home portal page.
  6. Download the manifest.
  7. Install the plugin

Activate the ServiceNow Add-in for Microsoft Office

Follow this procedure from the product documentation.

Create a Record in the sn_office_control table.

Credit to Streyda.eu for outlining this procedure.

Note that this record should be created in the ServiceNow Add-Ins for Microsoft Office scope

In the Relative URL field, put the id of the page you will create later on (outook_create_story)

Create a View called “Outlook” on the rm_story Table.

Create a single column view with the fields you would like displayed inside the plugin. I created this view in the ServiceNow Add-Ins for Microsoft Office scope just to keep things consistent. Note that the rm_story has a view rule which returns the scrum view most of the time… this will prevent the Outlook view from displaying. Modify or disable this view rule to fix this.

Clone and modify the Create Incident Portal Widget

1. Clone the Create Incident portal widget

2. Rename it Create Story (ID=outlook_create_story)

3. Modify the server script. The widget works for both itil users and non itil users. If the user has the itil role, it provides the incident form, otherwise it displays the catalog item for creating an incident. In this case, we’re only going to be using the form since only fulfillers would be entering stories. If you’re doing this for another table, you can modify the server script to return the appropriate catalog item in line 32.

a. line 4 — change the role to the appropriate role for the table.  Here we’ll leave it as itil:  data.isITIL = gs.getUser().hasRole(‘itil’);

b. line 11 — change the table: data.table = ‘rm_story’;

here’s the whole server script

// form functionality - URL parameter driven
(function($sp, input, data, options, gs) {
	var catalogItemSysId;	
	data.isITIL = gs.getUser().hasRole('itil');
	data.isValid = false;
	data.maxAttachmentSize = parseInt(gs.getProperty("com.glide.attachment.max_size", 1024));
	if (isNaN(data.maxAttachmentSize))
		data.maxAttachmentSize = 24;
	
	if(data.isITIL) {
		//agent case
		data.table = 'rm_story';
		data.view = 'outlook';
		data.sys_id = "-1";
		if (input) {	
			data.table = input.table;
			data.sys_id = input.sys_id;
			if (input._fields) {
				result = $sp.saveRecord(input.table, input.sys_id, input._fields);
				data.sys_id = result.sys_id;
			}
		}

		data.f = $sp.getForm(data.table, data.sys_id, null, data.view);
		if(data.f) {
			data.isValid = true;			
		}
		
	} else {
		//self-service user case		
		catalogItemSysId = gs.getProperty('sn_outlook_addin.create_incident_cat_item', '3f1dd0320a0a0b99000a53f7604a2ef9');
		var validatedItem = new sn_sc.CatItem(catalogItemSysId);
		if (!validatedItem.canView() || !validatedItem.isVisibleServicePortal()) {
			data.recordFound = false;
			return;
		}
		data._generatedItemGUID = gs.generateGUID();
		data.sc_cat_item = $sp.getCatalogItem(catalogItemSysId, true);
		if(data.sc_cat_item && data.sc_cat_item.sys_class_name == 'sc_cat_item_producer') {
			var gr = new GlideRecord(data.sc_cat_item.sys_class_name);
			gr.get(data.sc_cat_item.sys_id);
			if (gr.isValidRecord())
				data.table = gr.getValue('table_name');
			
			data.isValid = true;
		}
	}
	// Add lua anlytics
	if(data.isValid && input && data.table) {
		var luaUtil = new sn_outlook_addin.SNCOutlookAddinUsageAnalytics();
		// For now since we know we are using LET hard code later may change to data.table + action
		luaUtil.incrementCounter('incident.create');
	}
	
})($sp, input, data, options, gs);

3. Modify the client script.

a.Change the $scope.messages at line 11

	$scope.messages = {
		"successMsg":"${Your story has been created in ServiceNow}",
		"form_not_available":"${This operation is currently not available.}",
		"create_new":"${Create New}",
		"view_incident":"${View Story}",
		"submit":"${Submit}",
		"delete_attachment":"${Delete Attachment?}"
	};

b. modify the populateFields function at line 107.  Here I’ve put the body of the email in the description field, along with the from, date, to and cc fields.  For more information on the email fields contained in $window.Office.context.mailbox.item, see this Microsoft docs page

if you’re just looking for put the body of the message into the description field, modify line 118: g_form.setValue(“description”, result.value.trim());

populateFields: function () {
		//OfficeJS integration - sets subject to short_description and body to comments field in the form.
		if ($scope.data.sys_id != "-1") {
			return;
		}
		var item;
		if ($window.Office && $window.Office.context && $window.Office.context.mailbox && $window.Office.context.mailbox.item) {
			try {
				item = $window.Office.context.mailbox.item;
				g_form.setValue("short_description", item.subject);
				item.body.getAsync("text", {}, function (result) {
					//g_form.setValue("description", result.value.trim());
					var mailBody = result.value.trim();

					function getAddressBlock(addresses) {
						var block = '';
						for (var i in addresses) {
							block += addresses[i].displayName + ' ' + addresses[i].emailAddress + ',';
						}
						return block.replace(/,\s*$/, "");
					}

					var desc = 'From: ' + item.from.emailAddress + '\n';
					desc += 'Sent: ' + item.dateTimeCreated + '\n';
					desc += 'To: ' + getAddressBlock(item.to) + '\n';
					desc += 'CC: ' + getAddressBlock(item.cc) + '\n\n';
					desc += mailBody;
					g_form.setValue('description', desc);
				});
			} catch (e) {
				//ignore this error
			}
		}

here’s the whole client script

function($scope, $timeout, $location, $window, spScUtil, spUtil, $rootScope, nowAttachmentHandler, spModal) {
	var g_form, showAlert, c = this, itilActions, endUserActions, ah;
	
	if($window.Office) {
		$window.Office.initialize = function() {
			if ($scope.data.isITIL && $scope.data.isValid){
				$scope.asITILUser().populateFields();		
			}
		};
	}
	$scope.messages = {
		"successMsg":"${Your story has been created in ServiceNow}",
		"form_not_available":"${This operation is currently not available.}",
		"create_new":"${Create New}",
		"view_incident":"${View Story}",
		"submit":"${Submit}",
		"delete_attachment":"${Delete Attachment?}"
	};
	
	$scope.alert=null;
	$scope.showConfirmation = false;
	$scope.submitting = false;
	$scope.isPageReady = false;
	
	//start attachment handler config
	ah = $scope.attachmentHandler = new nowAttachmentHandler(setAttachments, function(error){
		spUtil.addErrorMessage(error.msg + error.fileName);
	});
	ah.openSelector = function($event) {
		$event.stopPropagation();
		$event.preventDefault();
		var target = $($event.currentTarget);
		var input = target.parent().find('input');
		input.click();
	};
	
	function setAttachments(attachments, action) {
		$scope.attachments = attachments;
	}
	$scope.confirmDeleteAttachment = function(attachment) {
		spModal.confirm($scope.messages.delete_attachment).then(function() {
				$scope.attachmentHandler.deleteAttachment(attachment);
		});		
	}
	//end attachment handler config
	
	$scope.asITILUser = function() {		
		itilActions = itilActions || {
			getLatestIncident: function() {
					return $scope.data.f;
			},
			getPrimaryAction: function() {
				var primaryActions = $scope.data.f._ui_actions.filter(function(action) {
					return action.primary;
				});
				return (primaryActions.length) ? primaryActions[0] : null;
			},
			
			triggerUIAction: function() {
				var action = this.getPrimaryAction();
				if (!action.primary) {
					spUtil.addErrorMessage('${This action is not available}');
					return;
				}

				var activeElement = document.activeElement;
				if (activeElement) {
				  activeElement.blur();
				}

				$scope.$evalAsync(function() {
					if (g_form) {
						$scope.submitting = true;
						if (!g_form.submit(action.action_name || action.sys_id))
							$scope.submitting = false;
					  }
				});
			},
			initForm: function(formInstance) {
				//agent form loaded
				if(formInstance.getTableName() == $scope.data.f.table) {
					window.form = g_form = formInstance;
					this.populateFields();
					this._setAttachmentHandlerParams();
					$scope.isPageReady = true;
				}
			},
			_setAttachmentHandlerParams: function() {
				var tableId = ($scope.data.sys_id != -1 ? $scope.data.sys_id : ($scope.data.f ? $scope.data.f._attachmentGUID : -1));
				ah.setParams($scope.data.f.table, tableId, 1024 * 1024 * $scope.data.maxAttachmentSize);
				spUtil.recordWatch($scope, "sys_attachment", "table_sys_id=" + tableId, function(response, data) {
					$scope.attachmentHandler.getAttachmentList();		
				});
			},
			onFormSubmit: function(response) {
				var sysID;
				$scope.submitting = false;
				sysID = (response.isInsert) ? response.sys_id : $scope.data.sys_id;
				//sets the form in update mode from insert mode
				this.loadForm($scope.data.table, sysID).then(function(response) {
					if(response.isValid) {
						$scope.showConfirmation = true;
					}
				});
			},
			
			populateFields: function() {
				//OfficeJS integration - sets subject to short_description and body to comments field in the form.
				if($scope.data.sys_id != "-1") {
					return;
				}
				var item;
				if($window.Office && $window.Office.context && $window.Office.context.mailbox && $window.Office.context.mailbox.item) {
					try {
						item = $window.Office.context.mailbox.item;
						g_form.setValue("short_description", item.subject);
						item.body.getAsync("text", {}, function(result) {
							//g_form.setValue("description", result.value.trim());
							var mailBody = result.value.trim();
							
							function getAddressBlock(addresses){
								var block = '';
								for (var i in addresses){
									block += addresses[i].displayName + ' ' + addresses[i].emailAddress + ',';
								}
								return block.replace(/,\s*$/, "");
							}
							
							var desc = 'From: ' + item.from.emailAddress + '\n';
							desc += 'Sent: ' + item.dateTimeCreated + '\n';
							desc += 'To: ' + getAddressBlock(item.to) + '\n';
							desc += 'CC: ' + getAddressBlock(item.cc) + '\n\n';
							desc += mailBody;
							g_form.setValue('description',desc);
						});					
					} catch(e) {
						//ignore this error
					}
				}
				g_form.setValue('caller_id', $window.NOW.user_id);
			},
			loadForm: function(table, sys_id) {				
				$scope.data.table = table;
				$scope.data.sys_id = sys_id;
				return $scope.server.update();
			},
			clearConfirmation: function() {					
				$window.location.reload();
			}
		};
		return itilActions;
	};
	
	$scope.asEndUser = function() {
		var latestRecord;
		endUserActions = endUserActions || {
			
			getLatestIncident: function() {
				return latestRecord;
			},
			getRedirectUrl: function() {
				return latestRecord.redirect_url || (latestRecord.table +".do?sys_id="+latestRecord.sys_id);
			},
			clearForm: function() {
				if(g_form) {
					var i=0, fields = g_form.getFieldNames();
					for(;i<fields.length;i++){
						g_form.clearValue(fields[i]);
					}
				}
			},
			triggerUIAction: function() {				
				if(c.mandatory && c.mandatory.length > 0) {
					spUtil.addErrorMessage('${Provide input for all mandatory fields}');
					return;
				}
				var that = this;
				$scope.server.get({
					action: 'log_request',
					itemDetails: {sys_id: $scope.data.sc_cat_item.sys_id, 
										name: $scope.data.sc_cat_item.name,
										sys_class_name: $scope.data.sc_cat_item.sys_class_name}
				});
		
				spScUtil.submitProducer($scope.data.sc_cat_item.sys_id, this._getVarData($scope.data.sc_cat_item._fields), $scope.data._generatedItemGUID, $scope.data.workspaceParams).then(function(response) {
					latestRecord = response.data.result;
					$scope.showConfirmation = true;		
					that.clearForm();
					$scope.$emit("sp.form.submitted");
				});
			},
			initForm: function(formInstance) {
				//end user form loaded
				window.form = g_form = formInstance;				
				this._setAttachmentHandlerParams();
				$scope.isPageReady = true;
			},
			_setAttachmentHandlerParams: function() {
				var tableId = $scope.data._generatedItemGUID;				
				ah.setParams($scope.data.table, tableId, 1024 * 1024 * $scope.data.maxAttachmentSize);
				
				spUtil.recordWatch($scope, "sys_attachment", "table_sys_id=" + tableId, function(response, data) {
					$scope.attachmentHandler.getAttachmentList();		
				});
			},
			clearConfirmation: function() {					
				$window.location.reload();
			},
			_getVarData: function(fields) {
				var reqData = {};
				for(var obj in fields)
					reqData[fields[obj].name] = fields[obj].value;
				return reqData;
			}
			
		};
		
		return endUserActions;
	};
	
  
	$scope.$on("spModel.uiActionComplete", function(evt, response) {		
		if ($scope.data.isITIL && $scope.data.isValid){
			$scope.asITILUser().onFormSubmit(response);
		}
	});
  
	$scope.$on('spModel.gForm.initialized', function(e, gFormInstance) {
		if ($scope.data.isITIL && $scope.data.isValid){
			$scope.asITILUser().initForm(gFormInstance);
		} else if(!$scope.data.isITIL && $scope.data.isValid) {
			$scope.asEndUser().initForm(gFormInstance);
		}
	});
	

	
}

4. Modify the HTML Template. Change the href at line 11 to rm_story

 <a type="button" href="/rm_story.do?sys_id={{asITILUser().getLatestIncident().sys_id}}" target="_blank" class="btn btn-primary">{{messages.view_incident}}</a>

Copy and Modify the Outlook Home Portal Page

1. Copy the Outlook Home portal page. The ID of the new page should be outook_create_story

2. Replace the Create Incident widget with the Create Story widget

3. You should now be able to browse to the page and widget at /sncoutlook?id=outlook_create_story

Download the Manifest and Install the Plugin

Follow the procedure in this support article

Note that when you install the plugin in Outlook, you have to close the Outlook desktop app and reopen it for the plugin to appear. Sometimes you have to do this a few times.

When the plugin appears, you should see the portal page in Outlook after logging in to the instance