Added error handling, and refactored CopyAction slightly

This commit is contained in:
Henry 2015-10-29 16:40:51 -07:00
parent e37fa75289
commit 92a3fa3e4c
4 changed files with 137 additions and 117 deletions

View File

@ -52,7 +52,7 @@ define(
* are used to inform users of events in a non-intrusive way. As * are used to inform users of events in a non-intrusive way. As
* much as possible, notifications share a model with blocking * much as possible, notifications share a model with blocking
* dialogs so that the same information can be provided in a dialog * dialogs so that the same information can be provided in a dialog
* and then minimized to a banner notification if needed. * and then minimized to a banner notification if needed, or vice-versa.
* *
* @typedef {object} NotificationModel * @typedef {object} NotificationModel
* @property {string} title The title of the message * @property {string} title The title of the message
@ -75,6 +75,7 @@ define(
* @property {NotificationOption[]} options any additional * @property {NotificationOption[]} options any additional
* actions the user can take. Will be represented as additional buttons * actions the user can take. Will be represented as additional buttons
* that may or may not be available from a banner. * that may or may not be available from a banner.
* @see DialogModel
*/ */
/** /**
@ -220,7 +221,8 @@ define(
* @returns {Notification} the provided notification decorated with * @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize * functions to dismiss or minimize
*/ */
NotificationService.prototype.info = function (notificationModel) { NotificationService.prototype.info = function (model) {
var notificationModel = typeof model === "string" ? {title: model} : model
notificationModel.autoDismiss = notificationModel.autoDismiss || true; notificationModel.autoDismiss = notificationModel.autoDismiss || true;
notificationModel.severity = "info"; notificationModel.severity = "info";
return this.notify(notificationModel); return this.notify(notificationModel);

View File

@ -70,8 +70,12 @@ define(
* @param {string} verb the verb to display for the action (e.g. "Move") * @param {string} verb the verb to display for the action (e.g. "Move")
* @param {string} [suffix] a string to display in the dialog title; * @param {string} [suffix] a string to display in the dialog title;
* default is "to a new location" * default is "to a new location"
* @param {function} progressCallback a callback function that will
* be invoked to update invoker on progress. This is optional and
* may not be implemented by all composing actions. The signature of
* the callback function will depend on the service being invoked.
*/ */
function AbstractComposeAction(locationService, composeService, context, verb, suffix, progressCallback) { function AbstractComposeAction(locationService, composeService, context, verb, suffix) {
if (context.selectedObject) { if (context.selectedObject) {
this.newParent = context.domainObject; this.newParent = context.domainObject;
this.object = context.selectedObject; this.object = context.selectedObject;
@ -87,10 +91,9 @@ define(
this.composeService = composeService; this.composeService = composeService;
this.verb = verb || "Compose"; this.verb = verb || "Compose";
this.suffix = suffix || "to a new location"; this.suffix = suffix || "to a new location";
this.progressCallback = progressCallback;
} }
AbstractComposeAction.prototype.perform = function () { AbstractComposeAction.prototype.perform = function (progressCallback) {
var dialogTitle, var dialogTitle,
label, label,
validateLocation, validateLocation,
@ -98,7 +101,6 @@ define(
composeService = this.composeService, composeService = this.composeService,
currentParent = this.currentParent, currentParent = this.currentParent,
newParent = this.newParent, newParent = this.newParent,
progressCallback = this.progressCallback,
object = this.object; object = this.object;
if (newParent) { if (newParent) {

View File

@ -35,43 +35,57 @@ define(
* @memberof platform/entanglement * @memberof platform/entanglement
*/ */
function CopyAction(locationService, copyService, dialogService, notificationService, context) { function CopyAction(locationService, copyService, dialogService, notificationService, context) {
var notification, this.dialogService = dialogService;
this.notificationService = notificationService;
AbstractComposeAction.call(this, locationService, copyService, context, "Duplicate", "to a location");
}
CopyAction.prototype = Object.create(AbstractComposeAction.prototype);
CopyAction.prototype.perform = function() {
var self = this,
notification,
notificationModel = { notificationModel = {
title: "Copying objects", title: "Copying objects",
unknownProgress: false, unknownProgress: false,
severity: "info", severity: "info",
}; };
function progress(phase, totalObjects, processed){ function progress(phase, totalObjects, processed){
if (phase.toLowerCase() === 'preparing'){ if (phase.toLowerCase() === 'preparing'){
dialogService.showBlockingMessage({ self.dialogService.showBlockingMessage({
title: "Preparing to copy objects", title: "Preparing to copy objects",
unknownProgress: true, unknownProgress: true,
severity: "info", severity: "info",
}); });
} else if (phase.toLowerCase() === "copying") { } else if (phase.toLowerCase() === "copying") {
dialogService.dismiss(); self.dialogService.dismiss();
if (!notification) { if (!notification) {
notification = notificationService.notify(notificationModel); notification = self.notificationService.notify(notificationModel);
} }
notificationModel.progress = (processed / totalObjects) * 100; notificationModel.progress = (processed / totalObjects) * 100;
notificationModel.title = ["Copying ", processed, "of ", totalObjects, "objects"].join(" "); notificationModel.title = ["Copied ", processed, "of ", totalObjects, "objects"].join(" ");
if (processed >= totalObjects){ if (processed === totalObjects){
notification.dismiss(); notification.dismiss();
self.notificationService.info(["Successfully copied ", totalObjects, " items."].join(""));
} }
} }
} }
return new AbstractComposeAction(
locationService,
copyService,
context,
"Duplicate",
"to a location",
progress
);
}
AbstractComposeAction.prototype.perform.call(this, progress)
.then(function(){
self.notificationService.info("Copying complete.");
},
function (error){
//log error
//Show more general error message
self.notificationService.notify({
title: "Error copying objects.",
severity: "error",
hint: error.message
});
});
};
return CopyAction; return CopyAction;
} }
); );

View File

@ -55,85 +55,130 @@ define(
object.getCapability('type') object.getCapability('type')
); );
}; };
/**
* Will build a graph of an object and all of its composed objects in memory
* @private
* @param domainObject
*/
CopyService.prototype.buildCopyGraph = function(domainObject, parent) {
/* TODO: Use contextualized objects here.
Parent should be fully contextualized, and either the
original parent or a contextualized clone. The subsequent
composition changes can then be performed regardless of
whether it is the top level composition of the original
parent being updated, or of one of the cloned children. */
/**
* Will build a graph of an object and all of its child objects in
* memory
* @param domainObject The original object to be copied
* @param parent The parent of the original object to be copied
* @returns {Promise} resolved with an array of clones of the models
* of the object tree being copied. Copying is done in a bottom-up
* fashion, so that the last member in the array is a clone of the model
* object being copied. The clones are all full composed with
* references to their own children.
*/
CopyService.prototype.buildCopyPlan = function(domainObject, parent) {
var clones = [], var clones = [],
$q = this.$q, $q = this.$q,
self = this; self = this;
function clone(object) { function makeClone(object) {
return JSON.parse(JSON.stringify(object)); return JSON.parse(JSON.stringify(object));
} }
/**
* A recursive function that will perform a bottom-up copy of
* the object tree with originalObject at the root. Recurses to
* the farthest leaf, then works its way back up again,
* cloning objects, and composing them with their child clones
* as it goes
* @param originalObject
* @param originalParent
* @returns {*}
*/
function copy(originalObject, originalParent) { function copy(originalObject, originalParent) {
var modelClone = clone(originalObject.getModel()); //Make a clone of the model of the object to be copied
var modelClone = makeClone(originalObject.getModel());
modelClone.composition = []; modelClone.composition = [];
modelClone.id = uuid(); modelClone.id = uuid();
return $q.when(originalObject.useCapability('composition')).then(function(composees){
if (originalObject.hasCapability('composition')) { return (composees || []).reduce(function(promise, composee){
return originalObject.useCapability('composition').then(function(composees){ //If the object is composed of other
return composees.reduce(function(promise, composee){ // objects, chain a promise..
return promise.then(function(){ return promise.then(function(){
// ...to recursively copy it (and its children)
return copy(composee, originalObject).then(function(composeeClone){ return copy(composee, originalObject).then(function(composeeClone){
/* //Once copied, associate each cloned
TODO: Use the composition capability for this. Just not sure how to contextualize the as-yet non-existent modelClone object. // composee with its parent clone
*/
composeeClone.location = modelClone.id; composeeClone.location = modelClone.id;
return modelClone.composition.push(composeeClone.id); return modelClone.composition.push(composeeClone.id);
}); });
});}, $q.when(undefined)
).then(function (){
//Add the clone to the list of clones that will
//be returned by this function
clones.push({
model: modelClone,
persistenceSpace: originalParent.getCapability('persistence')
}); });
}, $q.when(undefined)).then(function (){
/* Todo: Move this outside of promise and avoid
duplication below */
clones.push({persistence: originalParent.getCapability('persistence'), model: modelClone});
return modelClone; return modelClone;
}); });
}); });
} else {
clones.push({persistence: originalParent.getCapability('persistence'), model: modelClone});
return $q.when(modelClone);
}
}; };
return copy(domainObject, parent).then(function(){ return copy(domainObject, parent).then(function(){
return clones; return clones;
}); });
} }
function newPerform (domainObject, parent, progress) { /**
* Will persist a list of {@link objectClones}.
* @private
* @param progress
* @returns {Function} a function that will perform the persistence
* with a progress callback curried into it.
*/
CopyService.prototype.persistObjects = function(progress) {
var persisted = 0,
self = this;
return function(objectClones) {
return self.$q.all(objectClones.map(function(clone, index){
return self.persistenceService.createObject(clone.persistenceSpace, clone.model.id, clone.model)
.then(function(){
progress("copying", objectClones.length, ++persisted);
});
})).then(function(){ return objectClones});
}
}
/**
* Will add a list of clones to the specified parent's composition
* @private
* @param parent
* @param progress
* @returns {Function}
*/
CopyService.prototype.addClonesToParent = function(parent, progress) {
var self = this;
return function(clones) {
var parentClone = clones[clones.length-1];
parentClone.model.location = parent.getId()
return self.$q.when(
parent.hasCapability('composition') &&
parent.getCapability('composition').add(parentClone.model.id)
.then(function(){
parent.getCapability("persistence").persist()
}));
}
}
/**
* Creates a duplicate of the object tree starting at domainObject to
* the new parent specified.
* @param domainObject
* @param parent
* @param progress
* @returns a promise that will be completed when the duplication is
* successful, otherwise an error is thrown.
*/
CopyService.prototype.perform = function (domainObject, parent, progress) {
var $q = this.$q, var $q = this.$q,
processed = 0,
self = this; self = this;
if (this.validate(domainObject, parent)) { if (this.validate(domainObject, parent)) {
progress("preparing"); progress("preparing");
return this.buildCopyGraph(domainObject, parent) return this.buildCopyPlan(domainObject, parent)
.then(function(clones){ .then(self.persistObjects(progress))
return $q.all(clones.map(function(clone, index){ .then(self.addClonesToParent(parent, progress));
return self.persistenceService.createObject(clone.persistence.getSpace(), clone.model.id, clone.model).then(function(){progress("copying", clones.length, processed++);});
})).then(function(){ return clones});
})
.then(function(clones) {
var parentClone = clones[clones.length-1];
parentClone.model.location = parent.getId()
return $q.when(
parent.hasCapability('composition') &&
parent.getCapability('composition').add(parentClone.model.id)
.then(function(){
progress("copying", clones.length, clones.length);
parent.getCapability("persistence").persist()
}));
});
} else { } else {
throw new Error( throw new Error(
"Tried to copy objects without validating first." "Tried to copy objects without validating first."
@ -141,49 +186,6 @@ define(
} }
} }
CopyService.prototype.perform = newPerform;
function oldPerform (domainObject, parent) {
var model = JSON.parse(JSON.stringify(domainObject.getModel())),
$q = this.$q,
self = this;
// Wrapper for the recursive step
function duplicateObject(domainObject, parent) {
return self.perform(domainObject, parent);
}
if (!this.validate(domainObject, parent)) {
throw new Error(
"Tried to copy objects without validating first."
);
}
if (domainObject.hasCapability('composition')) {
model.composition = [];
}
return this.creationService
.createObject(model, parent)
.then(function (newObject) {
if (!domainObject.hasCapability('composition')) {
return;
}
return domainObject
.useCapability('composition')
.then(function (composees) {
// Duplicate composition serially to prevent
// write conflicts.
return composees.reduce(function (promise, composee) {
return promise.then(function () {
return duplicateObject(composee, newObject);
});
}, $q.when(undefined));
});
});
};
return CopyService; return CopyService;
} }
); );