diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 00815faa1c..f5142031e6 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -156,14 +156,11 @@ define([ "name": "Save", "description": "Save changes made to these objects.", "depends": [ - "$q", - "$location", "$injector", - "urlService", - "navigationService", "policyService", "dialogService", - "creationService" + "creationService", + "copyService" ], "priority": "mandatory" }, diff --git a/platform/commonUI/edit/src/actions/SaveAction.js b/platform/commonUI/edit/src/actions/SaveAction.js index 073beeb4df..1415a6ed4b 100644 --- a/platform/commonUI/edit/src/actions/SaveAction.js +++ b/platform/commonUI/edit/src/actions/SaveAction.js @@ -36,18 +36,22 @@ define( * @implements {Action} * @memberof platform/commonUI/edit */ - function SaveAction($q, $location, $injector, urlService, navigationService, policyService, dialogService, creationService, context) { + function SaveAction( + $injector, + policyService, + dialogService, + creationService, + copyService, + context + ) { this.domainObject = (context || {}).domainObject; - this.$location = $location; this.injectObjectService = function(){ this.objectService = $injector.get("objectService"); }; - this.urlService = urlService; - this.navigationService = navigationService; this.policyService = policyService; this.dialogService = dialogService; this.creationService = creationService; - this.$q = $q; + this.copyService = copyService; } SaveAction.prototype.getObjectService = function(){ @@ -67,35 +71,29 @@ define( */ SaveAction.prototype.perform = function () { var domainObject = this.domainObject, - $location = this.$location, - urlService = this.urlService, + copyService = this.copyService, self = this; function resolveWith(object){ - return function() { + return function () { return object; }; } function doWizardSave(parent) { var context = domainObject.getCapability("context"), - wizard = new CreateWizard(domainObject, parent, self.policyService); + wizard = new CreateWizard( + domainObject, + parent, + self.policyService + ); return self.dialogService - .getUserInput(wizard.getFormStructure(true), wizard.getInitialFormValue()) - .then(function(formValue){ - return wizard.populateObjectFromInput(formValue, domainObject); - }); - } - - - function persistObject(object){ - - //Persist first to mark dirty - return object.getCapability('persistence').persist().then(function(){ - //then save permanently - return object.getCapability('editor').save(); - }); + .getUserInput( + wizard.getFormStructure(true), + wizard.getInitialFormValue() + ) + .then(wizard.populateObjectFromInput.bind(wizard)); } function fetchObject(objectId){ @@ -108,16 +106,18 @@ define( return fetchObject(object.getModel().location); } - function locateObjectInParent(parent){ - parent.getCapability('composition').add(domainObject.getId()); - return parent.getCapability('persistence').persist().then(function() { - return parent; - }); + function allowClone(objectToClone) { + return (objectToClone.getId() === domainObject.getId()) || + objectToClone.getCapability('location').isOriginal(); } - function doNothing() { - // Create cancelled, do nothing - return false; + function cloneIntoParent(parent) { + return copyService.perform(domainObject, parent, allowClone); + } + + function cancelEditingAfterClone(clonedObject) { + return domainObject.getCapability("editor").cancel() + .then(resolveWith(clonedObject)); } // Invoke any save behavior introduced by the editor capability; @@ -127,17 +127,13 @@ define( function doSave() { //This is a new 'virtual object' that has not been persisted // yet. - if (!domainObject.getModel().persisted){ + if (domainObject.getModel().persisted === undefined){ return getParent(domainObject) - .then(doWizardSave) - .then(persistObject) - .then(getParent)//Parent may have changed based - // on user selection - .then(locateObjectInParent) - .then(function(){ - return fetchObject(domainObject.getId()); - }) - .catch(doNothing); + .then(doWizardSave) + .then(getParent) + .then(cloneIntoParent) + .then(cancelEditingAfterClone) + .catch(resolveWith(false)); } else { return domainObject.getCapability("editor").save() .then(resolveWith(domainObject.getOriginalObject())); @@ -148,7 +144,7 @@ define( // UI, which will have been pushed atop the Browse UI.) function returnToBrowse(object) { if (object) { - self.navigationService.setNavigation(object); + object.getCapability("action").perform("navigate"); } return object; } diff --git a/platform/entanglement/src/services/CopyService.js b/platform/entanglement/src/services/CopyService.js index bc825c0394..56155f0b77 100644 --- a/platform/entanglement/src/services/CopyService.js +++ b/platform/entanglement/src/services/CopyService.js @@ -54,20 +54,52 @@ define( ); }; + /** + * A function used to check if a domain object should be cloned + * or not. + * @callback platform/entanglement.CopyService~filter + * @param {DomainObject} domainObject the object to be cloned + * @returns {boolean} true if the object should be cloned; false + * if it should be linked + */ + /** * Creates a duplicate of the object tree starting at domainObject to * the new parent specified. - * @param domainObject - * @param parent - * @param progress + * + * Any domain objects which cannot be created will not be cloned; + * instead, these will appear as links. If a filtering function + * is provided, any objects which fail that check will also be + * linked instead of cloned + * + * @param {DomainObject} domainObject the object to duplicate + * @param {DomainObject} parent the destination for the clone + * @param {platform/entanglement.CopyService~filter} [filter] + * an optional function used to filter out objects from + * the cloning process * @returns a promise that will be completed with the clone of * domainObject when the duplication is successful. */ - CopyService.prototype.perform = function (domainObject, parent) { - var $q = this.$q, - copyTask = new CopyTask(domainObject, parent, this.policyService, this.$q); + CopyService.prototype.perform = function (domainObject, parent, filter) { + var policyService = this.policyService; + + // Combines caller-provided filter (if any) with the + // baseline behavior of respecting creation policy. + function filterWithPolicy(domainObject) { + return (!filter || filter(domainObject)) && + policyService.allow( + "creation", + domainObject.getCapability("type") + ); + } + if (this.validate(domainObject, parent)) { - return copyTask.perform(); + return new CopyTask( + domainObject, + parent, + filterWithPolicy, + this.$q + ).perform(); } else { throw new Error( "Tried to copy objects without validating first." diff --git a/platform/entanglement/src/services/CopyTask.js b/platform/entanglement/src/services/CopyTask.js index 693acc2df3..4906d84b2e 100644 --- a/platform/entanglement/src/services/CopyTask.js +++ b/platform/entanglement/src/services/CopyTask.js @@ -31,18 +31,21 @@ define( * This class encapsulates the process of copying a domain object * and all of its children. * - * @param domainObject The object to copy - * @param parent The new location of the cloned object tree - * @param $q + * @param {DomainObject} domainObject The object to copy + * @param {DomainObject} parent The new location of the cloned object tree + * @param {platform/entanglement.CopyService~filter} filter + * a function used to filter out objects from + * the cloning process + * @param $q Angular's $q, for promises * @constructor */ - function CopyTask (domainObject, parent, policyService, $q){ + function CopyTask (domainObject, parent, filter, $q){ this.domainObject = domainObject; this.parent = parent; this.firstClone = undefined; this.$q = $q; this.deferred = undefined; - this.policyService = policyService; + this.filter = filter; this.persisted = 0; this.clones = []; this.idMap = {}; @@ -101,9 +104,14 @@ define( * Will add a list of clones to the specified parent's composition */ function addClonesToParent(self) { - self.parent.getCapability("composition").add(self.firstClone.getId()); - return self.parent.getCapability("persistence").persist() - .then(function(){return self.firstClone;}); + return self.parent.getCapability("composition") + .add(self.firstClone) + .then(function (addedClone) { + return self.parent.getCapability("persistence").persist() + .then(function () { + return addedClone; + }); + }); } /** @@ -193,7 +201,7 @@ define( //Check if the type of the object being copied allows for // creation of new instances. If it does not, then a link to the // original will be created instead. - if (this.policyService.allow("creation", originalObject.getCapability("type"))){ + if (this.filter(originalObject)) { //create a new clone of the original object. Use the // creation capability of the targetParent to create the // new clone. This will ensure that the correct persistence diff --git a/platform/entanglement/test/services/CopyServiceSpec.js b/platform/entanglement/test/services/CopyServiceSpec.js index 3d4ebf147e..fb5092ba65 100644 --- a/platform/entanglement/test/services/CopyServiceSpec.js +++ b/platform/entanglement/test/services/CopyServiceSpec.js @@ -162,6 +162,7 @@ define( 'compositionCapability', ['invoke', 'add'] ); + compositionCapability.add.andCallFake(synchronousPromise); locationCapability = jasmine.createSpyObj( 'locationCapability', @@ -387,6 +388,7 @@ define( expect(childObjectClone.getModel().location).toEqual(objectClone.getId()); }); }); + describe("when cloning non-creatable objects", function() { beforeEach(function () { policyService.allow.andCallFake(function(category){ @@ -401,8 +403,33 @@ define( it ("creates link instead of clone", function() { var copiedObject = copyFinished.calls[0].args[0]; expect(copiedObject).toBe(object); - expect(compositionCapability.add).toHaveBeenCalledWith(copiedObject.getId()); - //expect(newParent.getModel().composition).toContain(copiedObject.getId()); + expect(compositionCapability.add) + .toHaveBeenCalledWith(copiedObject); + }); + }); + + describe("when provided a filtering function", function () { + function accept() { + return true; + } + function reject() { + return false; + } + + it("does not create new instances of objects " + + "rejected by the filter", function() { + copyService.perform(object, newParent, reject) + .then(copyFinished); + expect(copyFinished.mostRecentCall.args[0]) + .toBe(object); + }); + + it("does create new instances of objects " + + "accepted by the filter", function() { + copyService.perform(object, newParent, accept) + .then(copyFinished); + expect(copyFinished.mostRecentCall.args[0]) + .not.toBe(object); }); }); }); diff --git a/platform/entanglement/test/services/CopyTaskSpec.js b/platform/entanglement/test/services/CopyTaskSpec.js index b63c72d6d2..a66fb185ce 100644 --- a/platform/entanglement/test/services/CopyTaskSpec.js +++ b/platform/entanglement/test/services/CopyTaskSpec.js @@ -44,11 +44,10 @@ define( describe("CopyTask", function () { var mockDomainObject, mockParentObject, - mockPolicyService, + mockFilter, mockQ, mockDeferred, testModel, - mockCallback, counter, cloneIds, task; @@ -119,17 +118,14 @@ define( mockParentObject = domainObjectFactory({ capabilities: makeMockCapabilities() }); - mockPolicyService = jasmine.createSpyObj( - 'policyService', - [ 'allow' ] - ); + mockFilter = jasmine.createSpy('filter'); mockQ = jasmine.createSpyObj('$q', ['when', 'defer', 'all']); mockDeferred = jasmine.createSpyObj( 'deferred', [ 'notify', 'resolve', 'reject' ] ); - mockPolicyService.allow.andReturn(true); + mockFilter.andReturn(true); mockQ.when.andCallFake(synchronousPromise); mockQ.defer.andReturn(mockDeferred); @@ -156,7 +152,7 @@ define( task = new CopyTask( mockDomainObject, mockParentObject, - mockPolicyService, + mockFilter, mockQ ); @@ -218,7 +214,7 @@ define( task = new CopyTask( mockComposingObject, mockParentObject, - mockPolicyService, + mockFilter, mockQ );