Collaboration Store, Part LIV

“Real happiness lies in the completion of work using your own brains and skills.”
Soichiro Honda

Last time, we finally got to the point where we were able to actually Commit the Update Set, installing the requested version of the application. That was a major milestone, but we are not done just yet. We still have to update the version and application records with the fact that this version has now been installed. That’s a server-side operation, so we will need to add yet one more client callable function to our installation utilities Script Include. As we did before, we will use the attachment ID to locate the version record and the associated application record and then update them both. We will also need to hunt down the installed application record so that we can link it to the application record in the Collaboration Store database. We can also use data extracted from the version and application records to build a final status message informing the operator that the installation process is now complete.

We’ll call our new function recordInstallation, and start out by creating a response object, defaulting the success property to false, and then grabbing the attachment ID parameter that was passed in the Ajax call.

recordInstallation: function() {
	var answer = {success: false};
	var sysId = this.getParameter('attachment_id');
	if (sysId) {
		...
	} else {
		answer.error = 'Missing required parameter: attachment_id';
	}
	return JSON.stringify(answer);
}

Assuming that we have a sys_id, we will use it to get the attachment record, and then use the data in the attachment record to fetch the version and application records.

var sysAttGR = new GlideRecord('sys_attachment');
if (sysAttGR.get(sysId)) {
	var versionGR = new GlideRecord(sysAttGR.getDisplayValue('table_name'));
	if (versionGR.get(sysAttGR.getDisplayValue('table_sys_id'))) {
		answer.versionId = versionGR.getUniqueValue();
		var applicationGR = versionGR.member_application.getRefRecord();
		...
	} else {
		answer.error = 'Version record not found for sys_id ' + sysAttGR.getDisplayValue('table_sys_id');
	}
} else {
	answer.error = 'Attachment record not found for sys_id ' + sysId;
}

Once we have the version record and the application record in hand, we will need to hunt down the newly installed system application record.

var sysAppGR = new GlideRecord('sys_app');
sysAppGR.addQuery('scope', applicationGR.getValue('scope'));
sysAppGR.query();
if (sysAppGR.next()) {
	...
} else {
	answer.error = 'System application record not found for scope ' + applicationGR.getValue('scope');
}

Once we have the system application record, then we know that the application has been installed, and we can format the final status message.

answer.statusMessage = 'Version <strong>' + versionGR.getDisplayValue('version') + '</strong> of application <strong>' + applicationGR.getDisplayValue('name') + '</strong> installed.';

Now we need to link the installed system application record to our application record.

applicationGR.application = sysAppGR.getUniqueValue();
applicationGR.update();

We also need to mark the version as being installed.

versionGR.installed = true;
versionGR.update();

One other thing that we will need to do is to go through any other version records associated with this application and make sure that none of those are any longer marked as installed.

versionGR.initialize();
versionGR.addQuery('member_application', applicationGR.getUniqueValue());
versionGR.addQuery('sys_id', '!=', answer.versionId);
versionGR.query();
while (versionGR.next()) {
	versionGR.installed = false;
	versionGR.update();
}

That completes the updates to the Collaboration Store records, so the only thing left to do at this point is to override our initial response object success value with true.

answer.success = true;

Putting it all together, the entire function looks like this:

recordInstallation: function() {
	var answer = {success: false};
	var sysId = this.getParameter('attachment_id');
	if (sysId) {
		var sysAttGR = new GlideRecord('sys_attachment');
		if (sysAttGR.get(sysId)) {
			var versionGR = new GlideRecord(sysAttGR.getDisplayValue('table_name'));
			if (versionGR.get(sysAttGR.getDisplayValue('table_sys_id'))) {
				answer.versionId = versionGR.getUniqueValue();
				var applicationGR = versionGR.member_application.getRefRecord();
				var sysAppGR = new GlideRecord('sys_app');
				sysAppGR.addQuery('scope', applicationGR.getValue('scope'));
				sysAppGR.query();
				if (sysAppGR.next()) {
					answer.statusMessage = 'Version <strong>' + versionGR.getDisplayValue('version') + '</strong> of application <strong>' + applicationGR.getDisplayValue('name') + '</strong> installed.';
					applicationGR.application = sysAppGR.getUniqueValue();
					applicationGR.update();
					versionGR.installed = true;
					versionGR.update();
					versionGR.initialize();
					versionGR.addQuery('member_application', applicationGR.getUniqueValue());
					versionGR.addQuery('sys_id', '!=', answer.versionId);
					versionGR.query();
					while (versionGR.next()) {
						versionGR.installed = false;
						versionGR.update();
					}
					answer.success = true;
				} else {
					answer.error = 'System application record not found for scope ' + applicationGR.getValue('scope');
				}
			} else {
				answer.error = 'Version record not found for sys_id ' + sysAttGR.getDisplayValue('table_sys_id');
			}
		} else {
			answer.error = 'Attachment record not found for sys_id ' + sysId;
		}
	} else {
		answer.error = 'Missing required parameter: attachment_id';
	}
	return JSON.stringify(answer);
}

That takes care of the server-side code; now we have to update the Client script in our UI Page to make the GlideAjax call to the function and then return the operator to the original version record where the Install button was first selected. At the end of the Commit process, we referenced an updateStoreData function, but did not create it. We will need to create this function now, and then we can make the call within that new function. Before we do that, though, I wanted to note a slight modification that I made to the HTML for that page to add an id tag to the H4 element that includes our status message. The reason that I wanted to do that was so that the final message would not only replace the wording, but it would also replace the loading image that indicated that there was an ongoing process. Once we get to this point, the processing is over, so I did not want to leave that spinning image up on the screen. Here is the modified version of that portion of the HTML:

<h4 style="padding: 30px;" id="final_status_text">
  &#160;
  <img src="/images/loading_anim4.gif" height="18" width="18"/>
  &#160;
  <span id="status_text">Previewing Uploaded Update Set ...</span>
</h4>

Now we can create our new updateStoreData function.

function updateStoreData() {
	document.getElementById('status_text').innerHTML = 'Updating Collaboration Store Database ...';
	var ga = new GlideAjax('ApplicationInstaller');
	ga.addParam('sysparm_name', 'recordInstallation');
	ga.addParam('attachment_id', attachmentId);
	ga.getXMLAnswer(finalizeInstallation);
}

Now we have referenced a finalizeInstallation function, so we will need to create that as well. This function simply adds that final status message to the page and then returns the operator back to the original version form page.

function finalizeInstallation(answer) {
	var result = JSON.parse(answer);
	document.getElementById('final_status_text').innerHTML = result.statusMessage;
	window.location.href = '/x_11556_col_store_member_application_version.do?sys_id=' + result.versionId;
}

Here is the full Client script for the UI Page from top to bottom.

var dataLossConfirmDialog;
var attachmentId = '';
var updateSetId = '';
var commitInProgress = false;

function onLoad() {
	attachmentId = document.getElementById('attachment_id').value;
	updateSetId = document.getElementById('remote_update_set_id').value;
	if (updateSetId) {
		previewRemoteUpdateSet();
	}
}

addLoadEvent(function() {
	onLoad();
});

function previewRemoteUpdateSet() {
	var MESSAGE_KEY_DIALOG_TITLE = "Update Set Preview";
	var MESSAGE_KEY_CLOSE_BUTTON = "Close";
	var MESSAGE_KEY_CANCEL_BUTTON = "Cancel";
	var MESSAGE_KEY_CONFIRMATION = "Confirmation";
	var MESSAGE_KEY_CANCEL_CONFIRM_DIALOG_TILE = "Are you sure you want to cancel this update set preview?";
	var map = new GwtMessage().getMessages([MESSAGE_KEY_DIALOG_TITLE, MESSAGE_KEY_CLOSE_BUTTON, MESSAGE_KEY_CANCEL_BUTTON, MESSAGE_KEY_CONFIRMATION, MESSAGE_KEY_CANCEL_CONFIRM_DIALOG_TILE]);
	var dialogClass = window.GlideModal ? GlideModal : GlideDialogWindow;
	var dd = new dialogClass("hierarchical_progress_viewer", false, "40em", "10.5em");

	dd.setTitle(map[MESSAGE_KEY_DIALOG_TITLE]);
	dd.setPreference('sysparm_ajax_processor', 'UpdateSetPreviewAjax');
	dd.setPreference('sysparm_ajax_processor_function', 'preview');
	dd.setPreference('sysparm_ajax_processor_sys_id', updateSetId);
	dd.setPreference('sysparm_renderer_expanded_levels', '0');
	dd.setPreference('sysparm_renderer_hide_drill_down', true);
	dd.setPreference('focusTrap', true);
	dd.setPreference('sysparm_button_close', map["Close"]);
	dd.on("executionStarted", function(response) {
		var trackerId = response.responseXML.documentElement.getAttribute("answer");

		var cancelBtn = new Element("button", {
			'id': 'sysparm_button_cancel',
			'type': 'button',
			'class': 'btn btn-default',
			'style': 'margin-left: 5px; float:right;'
		}).update(map[MESSAGE_KEY_CANCEL_BUTTON]);

		cancelBtn.onclick = function() {
			var dialog = new GlideModal('glide_modal_confirm', true, 300);
			dialog.setTitle(map[MESSAGE_KEY_CONFIRMATION]);
			dialog.setPreference('body', map[MESSAGE_KEY_CANCEL_CONFIRM_DIALOG_TILE]);
			dialog.setPreference('focusTrap', true);
			dialog.setPreference('callbackParam', trackerId);
			dialog.setPreference('defaultButton', 'ok_button');
			dialog.setPreference('onPromptComplete', function(param) {
				var cancelBtn2 = $("sysparm_button_cancel");
				if (cancelBtn2)
					cancelBtn2.disable();
				var ajaxHelper = new GlideAjax('UpdateSetPreviewAjax');
				ajaxHelper.addParam('sysparm_ajax_processor_function', 'cancelPreview');
				ajaxHelper.addParam('sysparm_ajax_processor_tracker_id', param);
				ajaxHelper.getXMLAnswer(_handleCancelPreviewResponse);
			});
			dialog.render();
			dialog.on("bodyrendered", function() {
				var okBtn = $("ok_button");
				if (okBtn) {
					okBtn.className += " btn-destructive";
				}
			});
		};

		var _handleCancelPreviewResponse = function(answer) {
			var cancelBtn = $("sysparm_button_cancel");
			if (cancelBtn)
				cancelBtn.remove();
		};

		var buttonsPanel = $("buttonsPanel");
		if (buttonsPanel)
			buttonsPanel.appendChild(cancelBtn);
	});

	dd.on("executionComplete", function(trackerObj) {
		dd.destroy();
		checkPreviewResults();
	});
	
	dd.render();
}

function checkPreviewResults() {
	document.getElementById('status_text').innerHTML = 'Evaluating Preview Results ...';
	var ga = new GlideAjax('ApplicationInstaller');
	ga.addParam('sysparm_name', 'evaluatePreview');
	ga.addParam('remote_update_set_id', updateSetId);
	ga.getXMLAnswer(commitUpdateSet);
}

function commitUpdateSet(answer) {
	var result = JSON.parse(answer);
	var message = '';
	if (result.accepted > 0) {
		if (result.accepted > 1) {
			message += result.accepted + ' Flagged Updates Accepted; ';
		} else {
			message += 'One Flagged Update Accepted; ';
		}
	}
	if (result.skipped > 0) {
		if (result.skipped > 1) {
			message += result.skipped + ' Flagged Updates Skipped; ';
		} else {
			message += 'One Flagged Update Skipped; ';
		}
	}
	message += 'Committing Update Set ...';
	document.getElementById('status_text').innerHTML = message;
	commitRemoteUpdateSet();
}

function commitRemoteUpdateSet() {
	if (commitInProgress) {
		return;
	}

	var ajaxHelper = new GlideAjax('com.glide.update.UpdateSetCommitAjaxProcessor');
	ajaxHelper.addParam('sysparm_type', 'validateCommitRemoteUpdateSet');
	ajaxHelper.addParam('sysparm_remote_updateset_sys_id', updateSetId);
	ajaxHelper.getXMLAnswer(getValidateCommitUpdateSetResponse);
}

function getValidateCommitUpdateSetResponse(answer) {
	try {
		if (answer == null) {
			console.log('validateCommitRemoteUpdateSet answer was null');
			return;
		}
		console.log('validateCommitRemoteUpdateSet answer was ' + answer);
		var returnedInfo = answer.split(';');
		var sysId = returnedInfo[0];
		var encodedQuery = returnedInfo[1];
		var delObjList = returnedInfo[2];
		if (delObjList !== "NONE") {
			console.log('showing data loss confirm dialog');
			showDataLossConfirmDialog(sysId, delObjList, encodedQuery);
		} else {
			console.log('skipping data loss confirm dialog');
			runTheCommit(sysId);
		}
	} catch (e) {
		console.log(e);
	}
}

function runTheCommit(sysId) {
	console.log('running commit on ' + sysId);
	commitInProgress = true;
	var ajaxHelper = new GlideAjax('com.glide.update.UpdateSetCommitAjaxProcessor');
	ajaxHelper.addParam('sysparm_type', 'commitRemoteUpdateSet');
	ajaxHelper.addParam('sysparm_remote_updateset_sys_id', sysId);
	ajaxHelper.getXMLAnswer(getCommitRemoteUpdateSetResponse);
}

function destroyDialog() {
	dataLossConfirmDialog.destroy();
}

function showDataLossConfirmDialog(sysId, delObjList, encodedQuery) {
	var dialogClass = typeof GlideModal != 'undefined' ? GlideModal : GlideDialogWindow;
	var dlg = new dialogClass('update_set_data_loss_commit_confirm');
	dataLossConfirmDialog = dlg;
	dlg.setTitle('Confirm Data Loss');
	if(delObjList == null) {
		dlg.setWidth(300);
	} else {
		dlg.setWidth(450);
	}
	dlg.setPreference('sysparm_sys_id', sysId);
	dlg.setPreference('sysparm_encodedQuery', encodedQuery);
	dlg.setPreference('sysparm_del_obj_list', delObjList);
	console.log('rendering data loss confirm dialog');
	dlg.render();
}

function getCommitRemoteUpdateSetResponse(answer) {
	try {
		if (answer == null) {
			return;
		}
		var map = new GwtMessage().getMessages(["Close", "Cancel", "Are you sure you want to cancel this update set?", "Update Set Commit", "Go to Subscription Management"]);
		var returnedIds = answer.split(',');
		var workerId = returnedIds[0];
		var sysId = returnedIds[1];
		var shouldRefreshNav = returnedIds[2];
		var shouldRefreshApps = returnedIds[3];
		var dialogClass = window.GlideModal ? GlideModal : GlideDialogWindow;
		var dd = new dialogClass("hierarchical_progress_viewer", false, "40em", "10.5em");
		dd.setTitle(map["Update Set Commit"]);
		dd.setPreference('sysparm_renderer_execution_id', workerId);
		dd.setPreference('sysparm_renderer_expanded_levels', '0');
		dd.setPreference('sysparm_renderer_hide_drill_down', true);
		dd.setPreference('sysparm_button_subscription', map["Go to Subscription Management"]);
		dd.setPreference('sysparm_button_close', map["Close"]);
		dd.on("bodyrendered", function(trackerObj) {
			var buttonsPanel = $("buttonsPanel");
			var table = new Element("table", {cellpadding: 0, cellspacing: 0, width : "100%"});
			buttonsCell = table.appendChild(new Element("tr")).appendChild(new Element("td"));
			buttonsCell.align = "right";
			buttonsPanel.appendChild(table);
			var closeBtn = $("sysparm_button_close");
			if (closeBtn) {
				closeBtn.disable();
			}
			var cancelBtn = new Element("button");
			cancelBtn.id = "sysparm_button_cancel";
			cancelBtn.type = "button";
			cancelBtn.innerHTML = map["Cancel"];
			cancelBtn.onclick = function() {
				var response = confirm(map["Are you sure you want to cancel this update set?"]);
				if (response != true) {
					return;
				}
				var ajaxHelper = new GlideAjax('UpdateSetCommitAjax');
				ajaxHelper.addParam('sysparm_type', 'cancelRemoteUpdateSet');
				ajaxHelper.addParam('sysparm_worker_id', workerId);
				ajaxHelper.getXMLAnswer(getCancelRemoteUpdateSetResponse);
			};
			buttonsCell.appendChild(cancelBtn);
		});

		dd.on("executionComplete", function(trackerObj) {
			dd.destroy();
			updateStoreData();
		});

		dd.render();
	} catch (e) {
		console.log(e);
	}
}

function getCancelRemoteUpdateSetResponse(answer) {
	if (answer == null) {
		return;
	}
}

function updateStoreData() {
	document.getElementById('status_text').innerHTML = 'Updating Collaboration Store Database ...';
	var ga = new GlideAjax('ApplicationInstaller');
	ga.addParam('sysparm_name', 'recordInstallation');
	ga.addParam('attachment_id', attachmentId);
	ga.getXMLAnswer(finalizeInstallation);
}

function finalizeInstallation(answer) {
	var result = JSON.parse(answer);
	document.getElementById('final_status_text').innerHTML = result.statusMessage;
	window.location.href = '/x_11556_col_store_member_application_version.do?sys_id=' + result.versionId;
}

That completes the installation process, the third and final major component of the Collaboration Store application. All that is left now is to release a new Update Set to the testers and see what kinds of bugs we can shake out of this thing before we actually produce an official 1.0 version of the app. If you have not had an opportunity to participate in the testing just yet, now might be a good time to jump in and see what you can find. Feedback of any kind is always welcome. We’ll get more into all of that next time out.