diff --git a/platform/commonUI/notification/src/NotificationService.js b/platform/commonUI/notification/src/NotificationService.js index 0e2be08626..d6fac910a7 100644 --- a/platform/commonUI/notification/src/NotificationService.js +++ b/platform/commonUI/notification/src/NotificationService.js @@ -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); diff --git a/platform/entanglement/src/actions/AbstractComposeAction.js b/platform/entanglement/src/actions/AbstractComposeAction.js index 1ceb469591..5538eec3be 100644 --- a/platform/entanglement/src/actions/AbstractComposeAction.js +++ b/platform/entanglement/src/actions/AbstractComposeAction.js @@ -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) { diff --git a/platform/entanglement/src/actions/CopyAction.js b/platform/entanglement/src/actions/CopyAction.js index 2bbfbe1cee..ad8f41a323 100644 --- a/platform/entanglement/src/actions/CopyAction.js +++ b/platform/entanglement/src/actions/CopyAction.js @@ -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; } ); diff --git a/platform/entanglement/src/services/CopyService.js b/platform/entanglement/src/services/CopyService.js index 36daad9e33..dbf25fbf97 100644 --- a/platform/entanglement/src/services/CopyService.js +++ b/platform/entanglement/src/services/CopyService.js @@ -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; } );