Collaboration Store, Part LXXIII

“A little more persistence, a little more effort, and what seemed hopeless failure may turn to glorious success.”
Elbert Hubbard

Last time, we went over some of the remaining problems with this iteration of the software, and stopped short of providing a remedy for one of the issues, the fact that there is no capacity to include an instance logo image during the initial set-up process. Today we will take a look at correcting that design flaw.

Not one to create anything from scratch when someone else might have already put in all of that effort, I looked around for a portal page that had an image upload feature already in place. It turns out that the stock user_profile page, which uses the User Profile widget, has just the sort of thing that I was thinking of.

Stock User Profile page with existing image upload feature

Starting with the HTML, I dug into the code for the page and identified this section as pertaining to the image and upload button.

<div class="row">
  <div class="avatar-extra-large avatar-container" style="cursor:default;">
    <div class="avatar soloAvatar bottom">
      <div class="sub-avatar mia" ng-style="avatarPicture"><i class="fa fa-user"></i></div>
    </div>
  </div>
</div>
<div class="row">
  <button ng-if="::connectEnabled()" ng-click="openConnectConversation()" type="button"
          class="btn btn-primary send-message"><span class="glyphicon glyphicon-comment pad-right"></span>${Message}</button>
  <!-- file upload -->
  <span ng-if="::data.isLoggedInUsersProfile">
    <input ng-show="false" type="file" accept="image/jpeg,image/png,image/bmp,image/x-windows-bmp,image/gif,image/x-icon,image/svg+xml" ng-file-select="attachFiles({files: $files})" />
    <button ng-click="uploadNewProfilePicture($event)"
            ng-keypress="uploadNewProfilePicture($event)" type="button"
            class="btn btn-default send-message">${Upload Picture}</button>
  </span>
</div>

It was all pretty close to what I wanted, but I did make a few little tweaks here and there and finally ended up with this.

<div class="row" style="padding: 15px;">
  <div class="avatar-extra-large avatar-container" style="cursor:default;">
    <div class="avatar soloAvatar bottom">
      <div class="sub-avatar mia" ng-style="instanceLogoImage"><i class="fa fa-image"></i></div>
    </div>
  </div>
</div>
<div class="row" style="padding: 15px;">
  <input ng-show="false" type="file" accept="image/jpeg,image/png,image/bmp,image/x-windows-bmp,image/gif,image/x-icon,image/svg+xml" ng-file-select="attachFiles({files: $files})" />
  <button ng-click="uploadInstanceLogoImage($event)"
          ng-keypress="uploadInstanceLogoImage($event)" type="button"
          class="btn btn-default send-message">${Upload Instance Logo Image}</button>
</div>

Without bothering to build out the referenced functions to make it all actually work, I could still pull the page up and see how it rendered out at this point, just to get an idea of how it was going to look.

New initial set-up page with added logo image feature

So that looks pretty good. Now we need to add the client-side code needed to make it work. As with the HTML, we will turn to the User Profile widget to find some code to start out with.

$scope.uploadNewProfilePicture = function($event) {
	$event.stopPropagation();
	if($event.keyCode === 9) {
		return;
	}
	var $el = $element.find('input[type=file]');
	$el.click();
}

$scope.attachFiles = function(files) {
	var file = files.files[0];

	var validImage = false;

	switch(file.type) {
		case 'image/jpeg':
		case 'image/png':
		case 'image/bmp':
		case 'image/x-windows-bmp':
		case 'image/gif':
		case 'image/x-icon':
		case 'image/svg+xml':
			validImage = true;
			break;
		default:
			break;
	}

	if(!validImage) {
		alert(file.name + " " + i18n.getMessage("isn't a recognized image file format"));
		return;
	}

	snAttachmentHandler.create("live_profile", $scope.data.liveProfileID).uploadAttachment(file, {
		sysparm_fieldname: "photo"
	}).then(function(response) {
		var obj = {};
		obj.newAvatarId = response.sys_id;
		$scope.avatarPicture = {
			'background-image': "url('" + response.sys_id + ".iix')",
			'color': 'transparent'
		};
		$rootScope.$broadcast("sp.avatar_changed", obj);
		// timeout is required for screen reader to pick up the message once file upload prompt is dismissed
		$timeout(function() {
		   spAriaUtil.sendLiveMessage(i18n.getMessage('Profile picture updated successfully'));
		}, 500);
	});
}

$scope.avatarPicture = "";

$http.get('/api/now/live/profiles/sys_user.' + $scope.data.sysUserID).then(function (response) {
	if (response.data.result && response.data.result.avatar){
		var avatarPicture = response.data.result.avatar.replace("?t=small", "");
		$scope.avatarPicture = {
			'background-image': "url('" + avatarPicture + "')",
			'color': 'transparent'
		};
	}
});

Once again, I went through the code and made a few minimal modifications to make things work for our purpose and ended up with this.

$scope.uploadInstanceLogoImage = function($event) {
	$event.stopPropagation();
	if($event.keyCode === 9) {
		return;
	}
	var $el = $element.find('input[type=file]');
	$el.click();
};

$scope.attachFiles = function(files) {
	var file = files.files[0];

	var validImage = false;

	switch(file.type) {
		case 'image/jpeg':
		case 'image/png':
		case 'image/bmp':
		case 'image/x-windows-bmp':
		case 'image/gif':
		case 'image/x-icon':
		case 'image/svg+xml':
			validImage = true;
			break;
		default:
			break;
	}

	if(!validImage) {
		alert(file.name + " " + i18n.getMessage("isn't a recognized image file format"));
		return;
	}

	snAttachmentHandler.create('x_11556_col_store_member_organization', $scope.data.sys_id).uploadAttachment(file, {
		sysparm_fieldname: "logo"
	}).then(function(response) {
		$scope.instanceLogoImage = {
			'background-image': "url('" + response.sys_id + ".iix')",
			'color': 'transparent'
		};
	});
};

$scope.instanceLogoImage = "";

All of that would work great just as it is except for one thing: the snAttachmentHandler.create function wants to attach the file to an existing record, and the current design of the process doesn’t actually create the record in the database until after the Host instance accepts the registration request. We had that same problem with the Update Set XML file during the application publishing process, and I ended up attaching the file to the system application record, and then copying it over to the version record later, once the version record was created. Here, though, there really isn’t a record to which it could be attached temporarily, so in order for this to work, I am going to have to go ahead and create the record up front, and then do an update instead of an insert once the Host has granted access to the community. That took a little bit of re-engineering, but once that was done, everything seemed to work out OK. This is something that really needs to be tested thoroughly, though, but just from my cursory check-out it appears to be functioning as desired.

Image upload function in operation

With that up and running, it is time to release yet another Update Set for testing purposes. As with the 0.7.1 release, this should be a straight drop-in replacement for the 0.7/0.7.1 Update Set, and all of the other artifacts should be OK just as they are.

More information on previewing, committing, and testing can be found here and here and here. As always, feedback of any kind in the comments section is welcome, encouraged, and very much appreciated. Any information on your experiences is always a treat, and will give us a little something to review next time out.

Collaboration Store, Part LXXII

“Sometimes when you innovate, you make mistakes. It is best to admit them quickly and get on with improving your other innovations.”
Steve Jobs

Last time, we put out a new version of the Scoped Application Update Set that addressed a few of the reported issues. There are still a few unresolved issues, however, and some that have not even been discussed as yet. One that was reported last time, which I have been able to replicate, is a Commit failure of an attempted insert on the sys_scope_privilege table.

Duplicate entry ‘5b9c19242f6030104425fcecf699b6ec-global-sys_app_module-sys_db…’ for key ‘source_scope_2’

This causes the Update Set commit to be reported as a failure, even though everything else was installed without issue and everything works just fine despite this particular problem. I searched through the Update Set hoping to find an update that was in there twice for this item, but it only appears once, so I am not sure why we would be getting a duplicate entry error. The entry says ‘INSERT OR UPDATE’, so you would think that it would know if it was already there and do an UPDATE instead of an INSERT, but that’s obviously not what is happening. I’m going to have to do a little more research to see if I can find the cause of this one. Even though it doesn’t seem to hurt anything, I don’t like it and I would like to figure out how to prevent that from happening.

The other thing that seems to happen to folks during the Preview phase is that they run into a number of issues that need to be addressed and the Preview never runs clean. Up to this point, my recommendation has been to just accept all remote updates and then do the Commit. There are two areas, though, where I now believe that it would be better to skip the updates rather than accept them. One is any update related to System Properties and the other is any update related to the left navigation menu items. Once you have gone through the set-up process, the property values have all been establish based on the information entered during set-up. You do not want to overlay those with an Update Set or you will have to go through the set-up process all over again. Also, once you complete the set-up process, the set-up menu item is deactivated and all of the other menu items are revealed. You don’t want that to be reverted either, so if you are installing an update over an existing installation that has already been set up, you should skip any update related to these two items. All of the rest, you should still go ahead and accept. I don’t like having to have these kinds of specialized instructions, though, so I am going to be looking at ways to restructure things so that this is not an issue. The goal would be to always have a clean Preview and a clean Commit, regardless of whether it was a fresh install or an update of an existing install.

One issue that I did attempt to correct in the most recent version was the attachment copy problem, a problem that resulted in the attachment of the wrong file to the version record. It turns out that this bad code was replicated a number of times and was only fixed in one place. To correct that problem, and to make future maintenance a little easier, I built a common attachment copy function that contained the corrected code, and then called that function from every place that previously had a copy of the incorrect logic.

copyAttachment: function(fromTable, fromId, toTable, toId, attachmentId) {
	var response = {success: false};

	var gsa = new GlideSysAttachment();
	var values = gsa.copy(fromTable, fromId, toId, toId);
	if (values.length > 0) {
		for (var i=0; i<values.length; i++) {
			var ids = values[i].split(',');
			if (ids[0] == attachmentId) {
				response.success = true;
				response.newId = ids[1];
				gsa.deleteAttachment(attachmentId);
			} else {
				gsa.deleteAttachment(ids[1]);
			}
		}
	} else {
		response.error = 'Unrecognized response from attachment copy: ' +  JSON.stringify(values) + '\nFrom table: ' + fromTable +'; From ID: ' + fromId + '; To table: ' + toTable +'; To ID: ' + toId + '; Sys ID: ' + attachmentId;
	}

	return response;
}

Once that was in place, I updated the functions that previously included that code to just call that function and evaluate the results.

processPhase4: function(answer) {
	var response = this.CSU.copyAttachment('sys_app', answer.appSysId, 'x_11556_col_store_member_application_version', answer.versionId, answer.attachmentId);
	if (response.success) {
		answer.attachmentId = response.newId;
	} else {
		answer = this.processError(answer, 'Copy of Update Set XML file failed - ' + response.error);
	}

	return answer;
}

This code works for both Update Set XML files as well as logo image files.

copyLogoImage: function(sysAppGR, applicationGR) {
	var logoId = '';

	var csu = new CollaborationStoreUtils();
	var response = csu.copyAttachment('ZZ_YYx_11556_col_store_member_application', applicationGR.getUniqueValue(), 'ZZ_YYsys_app', sysAppGR.getUniqueValue(), applicationGR.getValue('logo'));
	if (response.success) {
		logoId = response.newId;
	} else {
		gs.error('Copy of application logo image failed - ' + response.error);
	}

	return logoId;
}

Speaking of logo image files, when I updated everything to include logo images for each instance, I neglected to add an image upload function to the initial set-up process. Since the Host’s data for each Client instance is collected during the set-up process, no Client instances will have logo images on the Host, which means no logo images will ever be shared with other Clients. I need to fix that, and then release yet another bug-fix Update Set that will address as many of these issues as possible. Adding the image upload to the set-up process may get a little involved, so let’s save 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 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.