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
* much as possible, notifications share a model with blocking
* 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
* @property {string} title The title of the message
@ -75,6 +75,7 @@ define(
* @property {NotificationOption[]} options any additional
* actions the user can take. Will be represented as additional buttons
* that may or may not be available from a banner.
* @see DialogModel
*/
/**
@ -220,7 +221,8 @@ define(
* @returns {Notification} the provided notification decorated with
* 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.severity = "info";
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} [suffix] a string to display in the dialog title;
* 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) {
this.newParent = context.domainObject;
this.object = context.selectedObject;
@ -87,10 +91,9 @@ define(
this.composeService = composeService;
this.verb = verb || "Compose";
this.suffix = suffix || "to a new location";
this.progressCallback = progressCallback;
}
AbstractComposeAction.prototype.perform = function () {
AbstractComposeAction.prototype.perform = function (progressCallback) {
var dialogTitle,
label,
validateLocation,
@ -98,7 +101,6 @@ define(
composeService = this.composeService,
currentParent = this.currentParent,
newParent = this.newParent,
progressCallback = this.progressCallback,
object = this.object;
if (newParent) {

View File

@ -35,43 +35,57 @@ define(
* @memberof platform/entanglement
*/
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 = {
title: "Copying objects",
unknownProgress: false,
severity: "info",
};
function progress(phase, totalObjects, processed){
if (phase.toLowerCase() === 'preparing'){
dialogService.showBlockingMessage({
self.dialogService.showBlockingMessage({
title: "Preparing to copy objects",
unknownProgress: true,
severity: "info",
});
} else if (phase.toLowerCase() === "copying") {
dialogService.dismiss();
self.dialogService.dismiss();
if (!notification) {
notification = notificationService.notify(notificationModel);
notification = self.notificationService.notify(notificationModel);
}
notificationModel.progress = (processed / totalObjects) * 100;
notificationModel.title = ["Copying ", processed, "of ", totalObjects, "objects"].join(" ");
if (processed >= totalObjects){
notificationModel.title = ["Copied ", processed, "of ", totalObjects, "objects"].join(" ");
if (processed === totalObjects){
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;
}
);

View File

@ -55,85 +55,130 @@ define(
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 = [],
$q = this.$q,
self = this;
function clone(object) {
function makeClone(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) {
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.id = uuid();
if (originalObject.hasCapability('composition')) {
return originalObject.useCapability('composition').then(function(composees){
return composees.reduce(function(promise, composee){
return $q.when(originalObject.useCapability('composition')).then(function(composees){
return (composees || []).reduce(function(promise, composee){
//If the object is composed of other
// objects, chain a promise..
return promise.then(function(){
// ...to recursively copy it (and its children)
return copy(composee, originalObject).then(function(composeeClone){
/*
TODO: Use the composition capability for this. Just not sure how to contextualize the as-yet non-existent modelClone object.
*/
//Once copied, associate each cloned
// composee with its parent clone
composeeClone.location = modelClone.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;
});
});
} else {
clones.push({persistence: originalParent.getCapability('persistence'), model: modelClone});
return $q.when(modelClone);
}
});
};
return copy(domainObject, parent).then(function(){
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,
processed = 0,
self = this;
if (this.validate(domainObject, parent)) {
progress("preparing");
return this.buildCopyGraph(domainObject, parent)
.then(function(clones){
return $q.all(clones.map(function(clone, index){
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()
}));
});
return this.buildCopyPlan(domainObject, parent)
.then(self.persistObjects(progress))
.then(self.addClonesToParent(parent, progress));
} else {
throw new Error(
"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;
}
);