Collaboration Store, Part LXI

“I think it is often easier to make progress on mega-ambitious dreams. Since no one else is crazy enough to do it, you have little competition.”
Larry Page

Last time, we wrapped up all of the code in the shared functions to include the logo images whenever an instance or application is transferred from one instance to another. Now we need to take a look at those places where the shared functions are not currently being used and replace the code in those existing functions with calls to the shared functions. This will not only consolidate the code and ensure that the logo images will be included, but since we also added a logging feature to the shared functions, it will also ensure that the REST API activity gets recorded.

Our ApplicationPublisher Script Include contains individual functions for the 7 independent phases of the application publishing process. The first four are all internal, but the last three move the artifacts from the Client instance to the the Host, so we will want to rework each one of those. Before we do that, though, we will want to snag the image from the sys_app record and attach it to our application record, which is something that we can do in Phase 2, once the application record is available. We can insert a line into this code:

mbrAppGR.setValue('name', sysAppGR.getValue('name'));
mbrAppGR.setValue('scope', sysAppGR.getValue('scope'));
mbrAppGR.setValue('description', sysAppGR.getValue('short_description'));
mbrAppGR.setValue('current_version', sysAppGR.getValue('version'));
mbrAppGR.setValue('active', true);
mbrAppGR.update();

… and make it look like this:

mbrAppGR.setValue('name', sysAppGR.getValue('name'));
mbrAppGR.setValue('scope', sysAppGR.getValue('scope'));
mbrAppGR.setValue('description', sysAppGR.getValue('short_description'));
mbrAppGR.setValue('current_version', sysAppGR.getValue('version'));
mbrAppGR.setValue('active', true);
if (sysAppGR.getValue('logo') && !mbrAppGR.getValue('logo')) {
	mbrAppGR.setValue('logo', this.copyLogoImage(answer));
}
mbrAppGR.update();

Of course, now that we have done that, we will need to build a new copyLogoImage function. We already have a function that copies an attachment (Phase 4), and we can steal most of the code from that guy.

processPhase4: function(answer) {
	var gsa = new GlideSysAttachment();
	var values = gsa.copy('sys_app', answer.appSysId, 'x_11556_col_store_member_application_version', answer.versionId);
	gsa.deleteAttachment(answer.attachmentId);
	if (values.length > 0) {
		var ids = values[values.length - 1].split(',');
		if (ids[1]) {
			answer.attachmentId = ids[1];
		} else {
			answer = this.processError(answer, 'Unrecognizable response from attachment copy: ' + values);
		}
	} else {
		answer = this.processError(answer, 'Unrecognizable response from attachment copy: ' + values);
	}

	return answer;
}

We want to return the attachment sys_id and not the answer object, and we need to swap out the table names, but other than that, it looks pretty similar:

copyLogoImage: function(answer) {
	var logoId = '';

	var gsa = new GlideSysAttachment();
	var values = gsa.copy('ZZ_YYsys_app', answer.appSysId, 'ZZ_YYx_11556_col_store_member_application', answer.mbrAppId);
	if (values.length > 0) {
		var ids = values[values.length - 1].split(',');
		if (ids[1]) {
			logoId = ids[1];
		} else {
			answer = this.processError(answer, 'Unrecognizable response from logo attachment copy: ' + values);
		}
	} else {
		answer = this.processError(answer, 'Unrecognizable response from logo attachment copy: ' + values);
	}

	return logoId;
}

That will get the logo image from the Scoped Application attached to our application record during the application publishing process. Now we need to take a look at those functions that send the local artifacts over to the Host instance and see if we can alter them to use the newly modified shared function. The first of such functions is processPhase5, and it moves over the application record.

processPhase5: function(answer) {
	var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
	if (mbrAppGR.get(answer.mbrAppId)) {
		var host = gs.getProperty('x_11556_col_store.host_instance');
		var token = gs.getProperty('x_11556_col_store.active_token');
		var thisInstance = gs.getProperty('instance_name');
		var request  = new sn_ws.RESTMessageV2();
		request.setHttpMethod('get');
		request.setBasicAuth(this.WORKER_ROOT + host, token);
		request.setRequestHeader("Accept", "application/json");
		request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application?sysparm_fields=sys_id&sysparm_query=provider.instance%3D' + thisInstance + '%5Ename%3D' + encodeURIComponent(mbrAppGR.getDisplayValue('name')));
		var response = request.execute();
		if (response.haveError()) {
			answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		} else if (response.getStatusCode() == '200') {
			var jsonString = response.getBody();
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch (e) {
				answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
			}
			if (!answer.error) {
				var payload = {};
				payload.name = mbrAppGR.getDisplayValue('name');
				payload.scope = mbrAppGR.getDisplayValue('scope');
				payload.description = mbrAppGR.getDisplayValue('description');
				payload.current_version = mbrAppGR.getDisplayValue('current_version');
				payload.active = 'true';
				request  = new sn_ws.RESTMessageV2();
				request.setBasicAuth(this.WORKER_ROOT + host, token);
				request.setRequestHeader("Accept", "application/json");
				if (jsonObject.result && jsonObject.result.length > 0) {
					answer.hostAppId = jsonObject.result[0].sys_id;
					request.setHttpMethod('put');
					request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application/' + answer.hostAppId);
				} else {
					request.setHttpMethod('post');
					request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application');
					payload.provider = thisInstance;
				}
				request.setRequestBody(JSON.stringify(payload, null, '\t'));
				response = request.execute();
				if (response.haveError()) {
					answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
				} else if (response.getStatusCode() != 200 && response.getStatusCode() != 201) {
					answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
				} else {
					jsonString = response.getBody();
					jsonObject = {};
					try {
						jsonObject = JSON.parse(jsonString);
					} catch (e) {
						answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
					}
					if (!answer.error) {
						answer.hostAppId = jsonObject.result.sys_id;
					}
				}
			}
		} else {
			answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
		}
	} else {
		answer = this.processError(answer, 'Invalid Member Application sys_id: ' + answer.appSysId);
	}

	return answer;
}

The corresponding shared function is pushApplication in the CollaborationStoreUtils Script Include, which takes the application GlideRecord, the target instance GlideRecord, and the sys_id of the local instance on the target instance as arguments. We already have the application GlideRecord, but will need to fetch the GlideRecord for the Host instance and grab the sys_id of local instance on the Host instance before we can make the call. Assuming that we can create a couple of functions to gather up the information that we need, we can reduce the new processPhase5 function to this:

processPhase5: function(answer) {
	var applicationGR = new GlideRecord('x_11556_col_store_member_application');
	if (applicationGR.get(answer.mbrAppId)) {
		var targetGR = this.getHostInstanceGR();
		var csu = new CollaborationStoreUtils();
		answer.hostInstanceId = csu.getRemoteInstanceSysId(targetGR);
		var result = csu.pushApplication(applicationGR, targetGR, answer.hostInstanceId);
		if (result.error) {
			answer = this.processError(answer, 'Error returned from Host instance: ' + result.error_code() + ' - ' + result.error_message);
		} else if (result.parse_error) {
			answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + result.body);
		} else if (result.status != 200 && result.status != 201) {
			answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + result.status);
		} else {
			answer.hostAppId = result.obj.result.sys_id;
		}
	} else {
		answer = this.processError(answer, 'Invalid Member Application sys_id: ' + answer.mbrAppId);
	}

	return answer;
}

That simplifies the code quite a bit, and yet we will be doing more work, as we will be moving over the logo image and also logging all the REST API calls. Much better. Of course we still need to build out those function to gather up the required arguments, but those should both be fairly straightforward. To fetch the Host instance GlideRecord, I kept the function in the ApplicationPublisher Script Include, but I put the other one in the main CollaborationStoreUtils Script Include, as that involves another REST API call to the Host instance, and I want to keep all of the functions that do that together in the same place. Here is the getHostInstanceGR function in the ApplicationPublisher Script Include:

getHostInstanceGR: function() {
	var instanceGR = new GlideRecord('x_11556_col_store_member_organization');
	instanceGR.get('instance', gs.getProperty('x_11556_col_store.host_instance'));
	return instanceGR;
}

And here is the getRemoteInstanceSysId function in the CollaborationStoreUtils Script Include:

getRemoteInstanceSysId: function(targetGR) {
	var sysId = '';

	var result = {};
	result.url = 'https://' + targetGR.getDisplayValue('instance') + '.service-now.com/api/now/table/x_11556_col_store_member_organization?sysparm_fields=sys_id&sysparm_query=instance%3D' + gs.getProperty('instance_name');
	result.method = 'GET';
	var request = new sn_ws.RESTMessageV2();
	request.setEndpoint(result.url);
	request.setHttpMethod(result.method);
	request.setBasicAuth(this.WORKER_ROOT + targetGR.getDisplayValue('instance'), targetGR.getDisplayValue('token'));
	request.setRequestHeader('Content-Type', 'application/json');
	request.setRequestHeader('Accept', 'application/json');
	var response = request.execute();
	result.status = response.getStatusCode();
	result.body = response.getBody();
	if (result.body) {
		try {
			result.obj = JSON.parse(result.body);
		} catch (e) {
			result.parse_error = e.toString();
		}
	}
	result.error = response.haveError();
	if (result.error) {
		result.error_code = response.getErrorCode();
		result.error_message = response.getErrorMessage();
	} else if (result.obj && result.obj.result && result.obj.result.length > 0) {
		sysId = result.obj.result[0].sys_id;
	}
	this.logRESTCall(targetGR, result);

	return sysId;
}

So that takes care of the first of the three functions that need to modified to use the shared REST API functions. Now we just need to do the same thing for the other two, processPhase6 and processPhase7. That should be a little simpler now that we have done the first one, but it’s still a bit of work, so let’s save all of that for our next installment.

Collaboration Store, Part LX

“Failure is simply the opportunity to begin again, this time more intelligently.”
Henry Ford

Last time, I had to confess that the code that I put out didn’t actually work. At the time, I had tried several things to make it work, but none of those were successful. Since then, I have tried quite a few other things, but none of those were successful, either. Eventually, I had to actually read the documentation, which helps quite a bit, but for some reason, always seems to be my tactic of last resort. Anyway, as it turns out, I only had to make one small change to get the logo image to actually appear on the other side intact. This line from my original attempt:

request.setRequestBody(gsa.getContentBase64(attachmentGR));

… just had to be changed to this:

request.setRequestBodyFromAttachment(attachmentGR.getUniqueValue());

The setRequestBodyFromAttachment method of the sn_ws.RESTMessageV2 object accepts the sys_id of the attachment as an argument and does all of the heavy lifting of building the request body from the attachment file. Once I replaced the setRequestBody method with the setRequestBodyFromAttachment method, everything worked great. So that takes care of that little problem. Now, where were we?

Now that we have a working function to push over the images for both instances and applications, we need to go into the functions that push over the instances and applications and add a call to this function. Here is the common function created to push over an instance.

pushInstance: function(instanceGR, targetGR) {
	var result = {};

	var payload = {};
	payload.instance = instanceGR.getDisplayValue('instance');
	payload.name = instanceGR.getDisplayValue('name');
	payload.description = instanceGR.getDisplayValue('description');
	payload.email = instanceGR.getDisplayValue('email');
	payload.accepted = instanceGR.getDisplayValue('accepted');
	payload.active = instanceGR.getDisplayValue('active');
	payload.host = instanceGR.getDisplayValue('host');
	result.url = 'https://' + targetGR.getDisplayValue('instance') + '.service-now.com/api/now/table/x_11556_col_store_member_organization';
	result.method = 'POST';
	var request = new sn_ws.RESTMessageV2();
	request.setEndpoint(result.url);
	request.setHttpMethod(result.method);
	request.setBasicAuth(this.WORKER_ROOT + targetGR.getDisplayValue('instance'), targetGR.getDisplayValue('token'));
	request.setRequestHeader('Content-Type', 'application/json');
	request.setRequestHeader('Accept', 'application/json');
	request.setRequestBody(JSON.stringify(payload, null, '\t'));
	var response = request.execute();
	result.status = response.getStatusCode();
	result.body = response.getBody();
	if (result.body) {
		try {
			result.obj = JSON.parse(result.body);
		} catch (e) {
			result.parse_error = e.toString();
		}
	}
	result.error = response.haveError();
	if (result.error) {
		result.error_code = response.getErrorCode();
		result.error_message = response.getErrorMessage();
	}
	this.logRESTCall(targetGR, result, payload);

	return result;
}

As we did within the pushImageAttachment function, we can add an else to the if (result.error) condition and check to see if we need to send over the image. In this instance, not only do we need to make sure that the instance record was successfully sent over to the target instance, we also need to check if the instance record actually has a logo image. If it does, then we need to grab the image attachment record so that we can use it to make the call to the pushImageAttachment function.

result.error = response.haveError();
if (result.error) {
	result.error_code = response.getErrorCode();
	result.error_message = response.getErrorMessage();
} else {
	if (instanceGR.getValue('logo')) {
		if (result.status == '201' && result.obj) {
			var attachmentGR = new GlideRecord('sys_attachment');
			attachmentGR.get(instanceGR.getValue('logo'));
			this.pushImageAttachment(attachmentGR, targetGR, 'x_11556_col_store_member_organization', result.obj.result.sys_id);
		}
	}
}

For an application record, things get a little more complicated, as that can be either an insert or an update. Since the application record might already exist on the target system, not only do we need to make sure that the application has a logo image on the source instance, but we also need to check to make sure that the image doesn’t already exist on the target instance. Here is the code that I came up with to add to the pushApplication function.

result.error = response.haveError();
if (result.error) {
	result.error_code = response.getErrorCode();
	result.error_message = response.getErrorMessage();
} else {
	if (applicationGR.getValue('logo') > '') {
		if ((result.status == '200' || result.status == '201') && result.obj) {
			if (!result.obj.result.logo) {
				var attachmentGR = new GlideRecord('sys_attachment');
				attachmentGR.get(applicationGR.getValue('logo'));
				this.pushImageAttachment(attachmentGR, targetGR, 'x_11556_col_store_member_application', result.obj.result.sys_id);
			}
		}
	}
}

With those changes, pushing an instance record will also push over the instance’s logo image and pushing an application record will also push over that application’s logo image. At least, that will happen if you are using the shared functions built for that purpose. We are not yet using those everywhere, though, so now might be a good time to fix that. We built those functions so that they could be called from various places as needed, but we never went back and refactored the code to actually do that in all places. That sounds like a good project for our next installment.

Collaboration Store, Part LIX

“When something you make doesn’t work, it didn’t work, not you. You, you work. You keep trying.”
Zach Klein

Last time, we created a couple of new shared functions to send over a logo image and associate that image with its base record. Unfortunately, the function that sends over the image file doesn’t actually work. Yes, it creates an attachment record on the target system, and yes, that attachment gets linked to its base record, but the image itself does not come across correctly, and the resulting file is not a valid image file. Yes, I should have tested that before I stuck the code out there, but it all seemed as if it should work, so I just threw it out there without first giving it a try.

I tried a few things to get it to go, but none of them did the trick. I went back to the getContent method instead of getContentBase64, but that didn’t work, so I tried getContentStream, but that didn’t do it, either. Then I tried adding a Content-Transfer-Encoding: base64 header, but that didn’t help, no matter what method I used to snag the content. So, it’s back to the drawing board on that one to see if we can’t figure out how to get that working correctly.

In the meantime, I decided to start logging all of this REST API activity so that I would have some record of what’s been happening between the instances. I have long thought that there should be some form of activity log tracking all of the important things going on with the records, and I even built a table for that early on, but that table was never used. This time, though, I was looking for something specific to the REST API activity, which has a number of specific data points. So, I created a new table called REST API Log to start tracking every request and response.

New REST API Log table

Then I added the following function to create records in this new table.

logRESTCall: function (targetGR, result, payload) {
	var logGR = new GlideRecord('x_11556_col_store_rest_api_log');
	logGR.instance = targetGR.getUniqueValue();
	logGR.url = result.url;
	logGR.method = result.method;
	if (payload) {
		logGR.request_body = JSON.stringify(payload, null, '\t');
	}
	logGR.response_code = result.status;
	if (result.obj) {
		logGR.response_body =  JSON.stringify(result.obj, null, '\t');
	} else {
		logGR.response_body =  result.body;
	}
	logGR.error = result.error;
	logGR.error_code = result.error_code;
	logGR.error_message = result.error_message;
	logGR.parse_error = result.parse_error;
	logGR.insert();
}

Then, at the end of each common REST API function, I added this line right before the final return statement:

this.logRESTCall(targetGR, result, payload);

Now, not every REST API call in the system uses these common functions, but my intent is to go back and correct that wherever appropriate, so eventually that should cover most of them, and then I can see what I need to do with the rest to get that activity logged as well. But it’s a start, anyway.

So now I have to get busy figuring out how to get my logo image over to another instance successfully. I’m sure that there is a way to do that; I just haven’t figured it out yet. Hopefully, we can explain how that is done next time out.

Collaboration Store, Part LVIII

“Progress is not in enhancing what is, but in advancing toward what will be.”
Khalil Gibran

Last time, we laid out all of the work that will need to be done to incorporate the new logo fields into the various processes of our application. Now we need to get busy doing that work. To begin, we can create a common function to move a logo image from one instance to another. We already have a common function to move an XML Update Set attachment from one instance to another, so let’s take a quick look at that guy and see if there is anything there that we can salvage for our new purpose.

pushAttachment: function(attachmentGR, targetGR, remoteVerId) {
	var result = {};

	var gsa = new GlideSysAttachment();
	result.url = 'https://';
	result.url += targetGR.getDisplayValue('instance');
	result.url += '.service-now.com/api/now/attachment/file?table_name=x_11556_col_store_member_application_version&table_sys_id=';
	result.url += remoteVerId;
	result.url += '&file_name=';
	result.url += attachmentGR.getDisplayValue('file_name');
	result.method = 'POST';
	var request = new sn_ws.RESTMessageV2();
	request.setEndpoint(result.url);
	request.setHttpMethod(result.method);
	request.setBasicAuth(this.WORKER_ROOT + targetGR.getDisplayValue('instance'), targetGR.getDisplayValue('token'));
	request.setRequestHeader('Content-Type', attachmentGR.getDisplayValue('content_type'));
	request.setRequestHeader('Accept', 'application/json');
	request.setRequestBody(gsa.getContent(attachmentGR));
	var response = request.execute();
	result.status = response.getStatusCode();
	result.body = response.getBody();
	if (result.body) {
		try {
			result.obj = JSON.parse(result.body);
		} catch (e) {
			result.parse_error = e.toString();
		}
	}
	result.error = response.haveError();
	if (result.error) {
		result.error_code = response.getErrorCode();
		result.error_message = response.getErrorMessage();
	}

	return result;
}

Since we want our new function to work for both instance logo images and application logo images, we will want to pass in both the table name and the table sys_id to our new function. Since the table name is part of the end point URL, we will want to change this:

result.url = 'https://';
result.url += targetGR.getDisplayValue('instance');
result.url += '.service-now.com/api/now/attachment/file?table_name=x_11556_col_store_member_application_version&table_sys_id=';
result.url += remoteVerId;
result.url += '&file_name=';
result.url += attachmentGR.getDisplayValue('file_name');

… to this:

result.url = 'https://';
result.url += targetGR.getDisplayValue('instance');
result.url += '.service-now.com/api/now/attachment/file?table_name=ZZ_YY';
result.url += tableName;
result.url += '&table_sys_id=';
result.url += tableSysId;
result.url += '&file_name=';
result.url += attachmentGR.getDisplayValue('file_name');

In addition to using the passed table name in the URL, we also prepend the string ZZ_YY to the value. This is a convention of the Now Platform to hide the attachment icon from the record for that image. When you manually add a logo image to a record and then go take a look at that image record in the sys_attachment table, you can see that the system has automatically prepended the ZZ_YY string to the table name. We want to our process to behave in the same manner, so we do that here as well.

The other difference between our logo image attachment and the XML Update Set attachment is that the XML content is in plain text and our image is stored in binary. Fortunately, the GlideSysAttachment object that we are using has a built-in way of handling that, so we just need to change this:

request.setRequestBody(gsa.getContent(attachmentGR));

… to this:

request.setRequestBody(gsa.getContentBase64(attachmentGR));

Other than these two changes, we should be able to use the rest of the original function intact. That makes our new function now look like this:

pushImageAttachment: function(attachmentGR, targetGR, tableName, tableSysId) {
	var result = {};

	var gsa = new GlideSysAttachment();
	result.url = 'https://';
	result.url += targetGR.getDisplayValue('instance');
	result.url += '.service-now.com/api/now/attachment/file?table_name=ZZ_YY';
	result.url += tableName;
	result.url += '&table_sys_id=';
	result.url += tableSysId;
	result.url += '&file_name=';
	result.url += attachmentGR.getDisplayValue('file_name');
	result.method = 'POST';
	var request = new sn_ws.RESTMessageV2();
	request.setEndpoint(result.url);
	request.setHttpMethod(result.method);
	request.setBasicAuth(this.WORKER_ROOT + targetGR.getDisplayValue('instance'), targetGR.getDisplayValue('token'));
	request.setRequestHeader('Content-Type', attachmentGR.getDisplayValue('content_type'));
	request.setRequestHeader('Accept', 'application/json');
	request.setRequestBody(gsa.getContentBase64(attachmentGR));
	var response = request.execute();
	result.status = response.getStatusCode();
	result.body = response.getBody();
	if (result.body) {
		try {
			result.obj = JSON.parse(result.body);
		} catch (e) {
			result.parse_error = e.toString();
		}
	}
	result.error = response.haveError();
	if (result.error) {
		result.error_code = response.getErrorCode();
		result.error_message = response.getErrorMessage();
	}

	return result;
}

That still does not complete the process, however. In addition to creating the attachment record linked to the base record, the logo field on the base record contains the sys_attachment record sys_id as the value. Once we create the sys_attachment record on the target system using the above function, we need to grab the resulting sys_id and update the logo field value on the base record. For that, we will need yet another function to make an additional REST API call to the target system to make that update. Since the new image field on both tables is named logo, we should again be able to create a single function that will work for both use cases. The payload that we will be sending just needs to contain the one value that we intend to update:

updateLogoField: function(attachmentId, targetGR, tableName, tableSysId) {
	var result = {};
 
	var payload = {};
	payload.logo = attachmentId;
	...
 
	return result;
}

For the URL, we will need to use both the passed table name and the passed table sys_id.

result.url = 'https://';
result.url += targetGR.getDisplayValue('instance');
result.url += '.service-now.com/api/now/table/';
result.url += tableName;
result.url += '/';
result.url += tableSysId;

… and the rest of the function is just our standard REST API call and result check:

updateLogoField: function(attachmentId, targetGR, tableName, tableSysId) {
	var result = {};
 
	var payload = {};
	payload.logo = attachmentId;
	result.url = 'https://';
	result.url += targetGR.getDisplayValue('instance');
	result.url += '.service-now.com/api/now/table/';
	result.url += tableName;
	result.url += '/';
	result.url += tableSysId;
	result.method = 'PUT';
	request  = new sn_ws.RESTMessageV2();
	request.setEndpoint(result.url);
	request.setHttpMethod(result.method);
	request.setBasicAuth(this.WORKER_ROOT + targetGR.getDisplayValue('instance'), targetGR.getDisplayValue('token'));
	request.setRequestHeader('Content-Type', 'application/json');
	request.setRequestHeader('Accept', 'application/json');
	request.setRequestBody(JSON.stringify(payload, null, '\t'));
	response = request.execute();
	result.status = response.getStatusCode();
	result.body = response.getBody();
	if (result.body) {
		try {
			result.obj = JSON.parse(result.body);
		} catch (e) {
			result.parse_error = e.toString();
		}
	}
	result.error = response.haveError();
	if (result.error) {
		result.error_code = response.getErrorCode();
		result.error_message = response.getErrorMessage();
	}
 
	return result;
}

Now all we have to do is call this function from our pushImageAttachment function, but only if all went well and the attachment was successfully sent over. We already have the targetGR, tableName, and tableSysId arguments available, but we will need to extract the remote system’s attachmentId from the response body of our call to send over the attachment. We are already checking for an error condition here:

result.error = response.haveError();
if (result.error) {
	result.error_code = response.getErrorCode();
	result.error_message = response.getErrorMessage();
}

… so we should be able to just add an else condition to that if statement to make the call.

result.error = response.haveError();
if (result.error) {
	result.error_code = response.getErrorCode();
	result.error_message = response.getErrorMessage();
} else {
	if (result.status == '201' && result.obj) {
		this.updateLogoField(result.obj.result.sys_id, targetGR, tableName, tableSysId);
	}
}

So now we have a common function that will send over the logo image for both instance records and application records, and then update the base record with attachment’s local sys_id. That’s a good start, but there is lots more to do, so we will keep plowing ahead next time out.

Collaboration Store, Part LVII

“Slow, steady progress is better than fast, daily excuses.”
Robin Sharma

It has been quite some time since we last dealt with this project, but now that our little side trip with the SNH Data Table collection has finally wrapped up, it’s time to get back into it. One of the things that I have always felt was missing in the work thus far was the logo images, both for the participating instances and the shared applications. To address that problem, I added a field called logo of type image to both the Member Organization and Member Application tables.

New Logo field added to the Member Organization table

When you add a value to an image field on a record, it creates a related record on the sys_attachment table to store the image. This record will then have to be moved from one instance to another as part of the instance registration, application publishing, and instance syncing processes. This will involve quite a bit of work, so let’s break it all down and make sure that we have covered everything before we start getting into the code.

The logo on the Member Organization record will be entered by the operator when the instance is defined. The logo on the Member Application record, on the other hand, will come from the logo associated with the Scoped Application. We will need to add logic to the application publishing process to snag the image from the Scoped Application record and add it to the application record. Once the images have been included in their respective records, we will then need to add logic to any process that moves those records from one instance to another. This includes Client instances sending artifacts over to the Host as well as Host instances sending artifacts out to the individual Clients. We already created a set of stock processes for moving individual artifacts from instance to instance when we built the periodic sync process, but we never went back and updated the original processes to use those new stock processes, so now would probably a good time to address that, since we will be working with that code anyway.

So, it would probably be a good idea to make a quick list of all of things that will need to be done to fully integrate the images into all of the various processes involved with the app. To help organize the list, let’s break it down by the major functions of the application:

Registration Process

The instance registration process is where you define your instance, so this process will need to include the ability to upload a logo to be associated with the instance. Additionally, once the image has been uploaded, the process that registers the instance with the Host will need to be modified to send the image over once the instance record has been established, and the process that shares the new instance with all of the other instances will need to do that as well.

Application Publishing Process

Publishing an application sends the application to the Host, which in turn sends it out to all of the other instances in the community. These processes will also have to be updated to send over the image associated with the app after the application record has been established on the target instance.

Application Installation Process

The installation process establishes the Scoped Application record on the installing instance, so this process will need to be modified to add the application’s logo image to that record once it has been created. This should be a fairly straightforward attachment copy, but making that copy needs to be part of the installation process.

Instance Sync Process

The instance sync process runs periodically to ensure that all of the Client instances are in sync with the Host instance, and it sends over any missing artifacts that are not already present on a Client. This process will need to be modified to send over an instance logo when it sends over an instance record and to send over an application logo whenever it sends over an application record.

None of these modifications should be too complicated or difficult, but the work still needs to be undertaken, completed, and tested. And, as mentioned earlier, this will also be an excellent opportunity to refactor the code to use the stock functions that were created when building the instance sync process, so that work will need to be completed as well. Even though there shouldn’t be anything here that should present much of challenge, there is still a lot to get done here, so let’s get right to it next time out.

Refactoring the SNH Data Table Widget, Part IV

“Testing is an infinite process of comparing the invisible to the ambiguous in order to avoid the unthinkable happening to the anonymous.”
James Bach

Last time, we tested a number of the features of the refactored SNH Data Table collection, but there is still much to do before we can bundle this all up into a new Update Set and stuff it out on Share. Now that we know that the initial version that I put out there is missing a critical component, I’d like to wrap this up and replace it with the refactored version, so let’s get to it.

To test the bulk actions and other clicks that do not result in navigating to a new portal page, I took my original Button Click Handler Example widget, renamed it the Table Click Handler Example widget, and then reworked it to handle all four of the clickable features, reference links, aggregate columns, buttons and icons, and bulk actions. Here is the new Client script for the repurposed widget:

function (spModal, $rootScope) {
	var c = this;
	var eventNames = {
		referenceClick: 'data_table.referenceClick',
		aggregateClick: 'data_table.aggregateClick',
		buttonClick: 'data_table.buttonClick',
		bulkAction: 'data_table.bulkAction'
	};

	$rootScope.$on(eventNames.referenceClick, function(e, parms) {
		displayClickDetails(eventNames.referenceClick, parms);
	});

	$rootScope.$on(eventNames.aggregateClick, function(e, parms) {
		displayClickDetails(eventNames.aggregateClick, parms);
	});

	$rootScope.$on(eventNames.buttonClick, function(e, parms) {
		displayClickDetails(eventNames.buttonClick, parms);
	});

	$rootScope.$on(eventNames.bulkAction, function(e, parms) {
		displayClickDetails(eventNames.bulkAction, parms);
	});

	function displayClickDetails(eventName, parms) {
		var html = '<div>'; 
		html += ' <table>\n';
		html += '  <tbody>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Event: &nbsp;</td>\n';
		html += '    <td>' + eventName + '</td>\n';
		html += '   </tr>\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">Config: &nbsp;</td>\n';
		html += '    <td><pre>' + JSON.stringify(parms.config, null, 4) + '</pre></td>\n';
		html += '   </tr>\n';
		html += '   <tr>\n';
		html += '    <td class="text-primary">Item(s): &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);
	}
}

This will allow me to test a number of things, all with the same companion widget, which should save a little bit of time. The original button_test page has a number of options in the configuration, so let’s pull that guy up and click around and see what we can find out.

Original button_test page

One thing that I noticed right away was that the master checkbox was not working. It seems that when I rebuilt the core SNH Data Table widget from the latest version of the stock Data Table widget, I neglected to paste back in the following added function:

$scope.masterCheckBoxClick = function() {
	for (var i in c.data.list) {
		c.data.list[i].selected = c.data.master_checkbox;
	}
};

Putting that back solved that problem, but then I also discovered that I needed to add spModal to the function arguments so that the error message would come up when you tried to select a bulk action from the drop-down without selecting any of the records in the table. Once I got all that out of the way, I got back to testing the buttons and icons.

Clicking on the Status Check icon

So now when I click on a button or icon, the alert pops up with all of the information that is passed with the broadcast message, which gives you an idea of what you have to work with in your companion widget to take whatever action that you would like to take based on that information. We should be able to do the same thing when selecting one or more rows and then choosing a bulk action.

Selecting a bulk action

Looks like that is working as well, so I think things are looking pretty good at this point. There is another old test page that we can pull called snh_data_table that has a companion widget for one of the icons.

snh_data_table test page

Once again, we will need to update the Client script in the click handler widget to adapt to our new approach to click handling.

function(spModal, $rootScope) {
	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) {
		if (parms.config.name == 'icon') {
			spModal.open({widget: 'snh-user-profile', widgetInput: {sys_id: parms.sys_id}}).then(function() {
				//
			});
		}
	});
}

Since the button is configured to launch a new page and the icon is configured to pop up a modal dialog, we need to check to make sure that the button click is for the icon and not the button. Other than that, it is very similar to the other examples.

Clicking on the icon to bring up the modal dialog

So that works. Very nice. Obviously, there is a lot more that we could do here to check out every little thing, but I think that we have covered most of the high points, and given that the version that is out on Share right now contains a pretty significant flaw, I think I would like to roll the dice and toss this out there as is to resolve that issue. Hopefully, I will not miss any important artifacts this time! Here is the Update Set for those of you who would like to check it out. As always, please feel free to leave any feedback of any kind in the comments. Thanks!

SNH Data Table Widgets on Share, Corrected

“An essential part of being a humble programmer is realizing that whenever there’s a problem with the code you’ve written, it’s always your fault.”
Jeff Atwood

Well, it turns out that I missed a critical widget when I built my first Update Set to be posted out on Share the other day. The Table Selector widget is used in the Content Selector Configuration Editor to select a new table whenever you wanted to add a table to the configuration. Without that widget included in the Update Set, a blank modal pop-up comes up when you click on the Add a New Table button, which is obviously not helpful. I have added the widget to a corrected Update Set that I need to get out on the Share site, but I thought that I would post it here first to see if there was anything else that I needed to fix before I do that. Thanks for the feedback, by the way … it is always much appreciated.

By the way, I built the Update Set using the Add to Update Set Utility, which is an awesome tool that I cannot recommend highly enough. The only problem with that, of course, is that you have to remember to include all of your artifacts when you create an Update Set and start adding in all of the various parts and pieces for your project. If you are not careful, you can leave out an important piece, which I did, and that’s on me. Hopefully, this is the only one, but there is no guarantee …

Refactoring the SNH Data Table Widget, Part III

“More than the act of testing, the act of designing tests is one of the best bug preventers known.”
Boris Beizer

Now that we have refactored the various SNH Data Table widgets and added the missing support for clickable aggregate columns and conditional buttons and icons, it’s time to run everything through its paces and make sure that it all works. To begin, let’s just see if the stuff that was working is still working now that we have made all of these changes to the artifacts and added these new features. A good place to start would be with the User Directory, as that little project utilizes a number of different components that were affected by the changes.

Primary User Directory page

Well, that seems to work, still. Changing perspectives and selecting different states all seem to function as they should as well, as does paging through the data and sorting on the various columns. So far so good. This tests out the SNH Data Table from URL Definition widget as well as the underlying core SNH Data Table widget. In the User Directory, the Department column and the Location column reference links are mapped to two other pages that were built using the SNH Data Table from JSON Configuration widget, and clicking on the value in one of those columns should test out both the reference link handling and that other wrapper widget.

Department Roster page

So far, so good. Clicking on a Location value should test out the reference link handling from this wrapper widget, and also bring up yet another page using the second of the three wrapper widgets.

Location Roster page

Once again, everything seems to be behaving as it should using the two wrapper widgets tested so far. The third wrapper widget, the SNH Data Table from Instance Definition widget, is not used in the User Directory, but we have other test scenarios set up for that one. Before we move on, though we can go ahead and test one more thing while we are working with the User Directory: conditional icons. In the User Admin perspective, there is an icon defined to edit the user’s data. We can add a quick condition to that icon configuration and see how that works out. Just for testing purposes, let’s try the following expression:

item.department.display_value == 'Sales'

If that works, the edit icon should only appear for users who are assigned to the Sales department.

User Directory User Admin perspective with conditional icon

Well, that seems to work as well. Good deal. While we are here, we might as well click on the icon and see if the button click handling works as well as the reference link handling that we tested earlier.

User Profile edit page

And it would seem that it does. That’s about all that we can squeeze out of the User Directory, but we still have a lot more to test, including the third wrapper widget and the other new feature, the clickable aggregate columns. Plus, we still have to do regression testing for bulk actions and clicks that result in a modal pop-up box instead of branching to a new portal page. That’s a lot to get through, so let’s keep plowing ahead.

Our third aggregate column test page was built on the SNH Data Table from Instance Definition widget, so let’s pull that guy up and see how things look.

Aggregate Column test page #3

Well, that all seems to work, and the Network group has two Incidents, so we just need another page to which we can link to display those Incidents. That’s easy enough to do with yet another aggregate column test page using the SNH Data Table from Instance Definition widget, a table name of Incident and a filter of active=true^assignment_group={{sys_id}}.

Aggregate Column test page #5

So all of three of the wrapper widgets seem to work, which by default tests out the core widget, and direct links from reference columns, buttons, and aggregate columns all navigate to the appropriate pages. That just leaves bulk actions and modal pop-ups to be tested, both of which will require companion widgets to listen for the associated broadcast messages. For that, we will have to hunt down or create some companion widgets for testing purposes, which might get a little involved, so let’s jump into that next time out.

Conditional Buttons and Icons on the SNH Data Table Widget

“Become a beacon of enhancement, and then, when the night is gray, all of the boats will move towards you, bringing their bountiful riches.”
James Altucher

After I posted the SNH Data Table widget collection out on Share, there were a couple of things that I felt were missing in the version that I put out there. One of those was the ability to click on an aggregate column and pull up a list of the records represented by that value. I took care of that one here, but after having second thoughts, the version that I ended up with is not quite like the one described in that installment of the series. Still, after a litter refactoring, I got it working in a way that finally seemed to be acceptable. But there is still one more thing that I would like to add before I release another version of the collection: the ability to control the presence of a button or icon on the row based on some condition.

Right now, if you configure a button or icon, that button or icon appears on every row of the table. That’s a nice feature that the stock Data Table widget does not include, but it would be even nicer if you could control whether or not the button appeared based on some condition. My thought was that I could add yet one more property to the button/icon configuration object called condition that could be used to control the presence of the action item on the row. I wasn’t exactly sure how to make that happen, but as usual, I thought that I would tackle the easiest portion first, which would be to modify the Content Selector Configurator widget to include that additional property. We just went through that exercise when adding the action property to the aggregate column configuration object (which was later replaced with the hint and page_id properties during refactoring), so the exercise is virtually the same.

As we did with the new aggregate column configuration property, we can start with the main widget and add the following to the list of column headings for the buttons and icons section of the table specification:

<th style="text-align: center;">${Condition}</th>

And then in the repeating rows of that table, we can insert this line:

<td data-th="${Condition}">{{btn.condition}}<

On the client side, we can add the following line to the new record section of the editButton() function:

shared.condition = button.condition;

… and this line to the section that saves the edits in that same function:

button.condition = shared.condition || '';

Finally, on the server side, in the Save() function that rebuilds the script, let’s add these lines in the button/icon specification section:

script += "',\n                 condition: '";
script += thisButton.condition;

That will handle things in most cases, but since this particular property is a Javascript expression, we need to account for the fact that the value might contain single quotes, and surrounding a value containing single quotes with single quotes will result in a syntax error in our Script Include. We should at least check for that, and if single quotes are present in the value, we should escape them. Instead of the script fragment above, which has been working for us in all other cases, let’s expand that a little bit to accommodate our concerns to something like this:

script += "',\n					condition: '";
if (thisButton.condition) {
	var condition = thisButton.condition;
	if (condition.indexOf("'") != -1) {
		condition = condition.replace(/'/g, "\\'");
	}
	script += condition;
}

That takes care of the main widget, but we also need to add a new input field to the pop-up editor before this will actually work, so we need to add this line to the HTML of the Button/Icon Editor widget:

<snh-form-field snh-model="c.widget.options.shared.condition" snh-name="condition"/>

That updates the editor to now include a new button/icon specification property called condition. Of course, that doesn’t actually add any functionality to the Data Table widgets just yet, but at least now we can add a value to that property through the editor, which is a start. Now let’s take a look at the actual SNH Data Table widget and see what we need to do in order to leverage that new property.

Once again, the easiest place to start is with the HTML. Here is the current section of the HTML that deals with buttons and icons:

<td ng-repeat="button in data.btnarray" role="cell" class="text-nowrap center" ng-class="{selected: item.selected}" tabindex="0">
  <a ng-if="!button.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonClick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">{{button.label}}</a>
  <a ng-if="button.icon" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonClick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">
    <span class="icon icon-{{button.icon}}" aria-hidden="true"></span>
    <span class="sr-only">{{button.hint}}</span>
  </a>
</td>

In the current version, there are two mutually exclusive anchor tags controlled by ng-if attributes that look to see whether or not an icon image was specified. We should be able to logically and our new condition to the existing conditions without disturbing the rest of the existing structure. The easiest way to do that at this point would simply be to call a function that dealt with the condition and have it return true or false based on the contents of our new condition property. We will have to build that function, but for now, we can just assume that it exists and modify the above to now look like this:

<td ng-repeat="button in data.btnarray" role="cell" class="text-nowrap center" ng-class="{selected: item.selected}" tabindex="0">
  <a ng-if="!button.icon && buttonCondition(button.condition, item)" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonClick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">{{button.label}}</a>
  <a ng-if="button.icon && buttonCondition(button.condition, item)" href="javascript:void(0)" role="button" class="btn-ref btn btn-{{button.color || 'default'}}" ng-click="buttonClick(button.name, item)" title="{{button.hint}}" data-original-title="{{button.hint}}">
    <span class="icon icon-{{button.icon}}" aria-hidden="true"></span>
    <span class="sr-only">{{button.hint}}</span>
  </a>
</td>

That should take care of the HTML. Now we need to come up with a function that will do what we want to do, which is to return true if we want the button/icon to appear and false if we do not. We can start out with something like this:

$scope.buttonCondition = function(expression, item) {
	var response = true;
	if (expression) {
		// check to see if the condition is true or false
	}
	return response;
};

This defaults the response to true, and if there is no condition specified, then the response will be true. If there is a condition, then we need to see if that condition is true for this item. Basically, we want to run the code that is stored in the property value. For that, we can use $scope.$eval. This AngularJS function will run the expression and return the result, which will then become our response. That will make our function look like this:

$scope.buttonCondition = function(expression, item) {
	var response = true;
	if (expression) {
		response = $scope.$eval(expression, this);
	}
	return response;
};

And that should be that! Now all we need to do is test all of this out, plus do a whole lot of regression testing for all of the refactoring, and then we can finally put out a new Update Set. That is definitely quite a bit of work in an of itself, so let’s save all of that for a future installment.

Refactoring the SNH Data Table Widget, Part II

“An intuitive definition is that a safe refactoring is one that doesn’t break a program. Because a refactoring is intended to restructure a program without changing its behavior, a program should perform the same way after a refactoring as it does before.”
Martin Fowler

Last time, we began the work of cleaning up the SNH Data Table widgets by consolidating all of the added action functions and bringing in the latest version of the original widget. To complete the work, we need to do the same for the remaining wrapper widgets in the collection, two of which were cloned from existing stock components, with the third being a new addition having no original source (although it was actually cloned from the modified version of one of the other two). As usual, we will start with the easy one first, the SNH Data Table from Instance Definition, cloned from the stock Data Table from Instance Definition widget.

The biggest change here was the addition of four new options to allow the entry of JSON strings for configuring each of the four new features:

[{"hint":"If enabled, show the list filter in the breadcrumbs of the data table",
"name":"enable_filter",
"default_value":"false",
"section":"Behavior",
"label":"Enable Filter",
"type":"boolean"},
{"hint":"A JSON object containing the specifications for aggregate data columns",
"name":"aggregates",
"default_value":"",
"section":"Behavior",
"label":"Aggregate Column Specifications (JSON)",
"type":"String"},
{"hint":"A JSON object containing the specifications for row-level buttons and action icons",
"name":"buttons",
"default_value":"",
"section":"Behavior",
"label":"Button/Icon Specifications (JSON)",
"type":"String"},
{"hint":"A JSON object containing the page id for any reference column links",
"name":"refpage",
"default_value":"",
"section":"Behavior",
"label":"Reference Page Specifications (JSON)",
"type":"String"}]

Other than that, the only other modification to the code required was in the Server script where we needed to point to the SNH core widget instead of the stock core widget:

// Start: SNH Data Table enhancements
	data.dataTableWidget = $sp.getWidget('snh-data-table', options);
// End: SNH Data Table enhancements

Aside from that single line, the remainder of the widget, including the entire Client script, remains the same as the latest version of the original widget. Next, we need to take a look at the SNH Data Table from URL Definition, which was cloned from the stock Data Table from URL Definition widget. As with the previous widget, the Client script from the latest version of the source widget remains the same. However, the Server script needs a little more modification than just the ID of the embedded core widget.

// Start: SNH Data Table enhancements
	data.fields = $sp.getParameter('fields') || $sp.getListColumns(data.table, data.view);
	copyParameters(data, ['aggregates', 'buttons', 'refpage', 'bulkactions']);
	data.show_new = options.show_new == true || options.show_new == "true";
	data.show_breadcrumbs = options.show_breadcrumbs == true || options.show_breadcrumbs == "true";
	data.window_size = $sp.getParameter('maximum_entries');
	data.btns = data.buttons;
	data.dataTableWidget = $sp.getWidget('snh-data-table', data);
// End: SNH Data Table enhancements

I left the original code intact, even in areas where the new code reset the values established differently in the original code, mainly because that didn’t really hurt anything and I was trying to retain the original as closely as possible to the way that it was for future comparisons.

That leaves the SNH Data Table from JSON Configuration, which does not have a stock version, although I did clone it originally from the modified SNH Data Table from URL Definition widget. Since there were no changes needed in the Client script of the other two widgets cloned from stock widgets, I went ahead and just copied the latest version of the Client script from the stock Data Table from URL Definition widget and pasted it into he Client script of the SNH Data Table from JSON Configuration widget. The rest was mostly custom code anyway, so I just left that alone.

That takes care of the three wrapper widgets, so now everything has been brought up the latest versions and the code for handling the four added features has all be consolidated into the core widget using a common function. That cleaned things up quite nicely, but I’m still not quite ready to spin up a new Update Set just yet. There is one more thing that I think needs to addressed before we do that.