diff --git a/platform/commonUI/browse/src/creation/CreateAction.js b/platform/commonUI/browse/src/creation/CreateAction.js index d294814304..9ac86d2dc8 100644 --- a/platform/commonUI/browse/src/creation/CreateAction.js +++ b/platform/commonUI/browse/src/creation/CreateAction.js @@ -87,10 +87,9 @@ define( newObject; newModel.type = this.type.getKey(); + newModel.location = parentObject.getId(); newObject = parentObject.useCapability('instantiation', newModel); - newObject.useCapability('mutation', function(model){ - model.location = parentObject.getId(); - }); + editorCapability = newObject.getCapability("editor"); if (countEditableViews(newObject) > 0 && newObject.hasCapability('composition')) { @@ -101,7 +100,7 @@ define( return newObject.useCapability("action").perform("save").then(function () { return editorCapability.save(); }, function () { - return editorCapability.cancel() + return editorCapability.cancel(); }); } }; diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 35818bd477..5a5101f129 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -41,9 +41,8 @@ define([ "./src/representers/EditRepresenter", "./src/representers/EditToolbarRepresenter", "./src/capabilities/EditorCapability", - "./src/capabilities/TransactionDecorator", + "./src/capabilities/TransactionCapabilityDecorator", "./src/services/TransactionService", - "./src/services/DirtyModelCache", "text!./res/templates/library.html", "text!./res/templates/edit-object.html", "text!./res/templates/edit-action-buttons.html", @@ -71,9 +70,8 @@ define([ EditRepresenter, EditToolbarRepresenter, EditorCapability, - TransactionDecorator, + TransactionCapabilityDecorator, TransactionService, - DirtyModelCache, libraryTemplate, editObjectTemplate, editActionButtonsTemplate, @@ -136,8 +134,7 @@ define([ "depends": [ "$location", "navigationService", - "$log", - "$q" + "$log" ], "description": "Edit this object.", "category": "view-control", @@ -270,11 +267,10 @@ define([ { "type": "decorator", "provides": "capabilityService", - "implementation": TransactionDecorator, + "implementation": TransactionCapabilityDecorator, "depends": [ "$q", - "transactionService", - "dirtyModelCache" + "transactionService" ] }, { @@ -283,15 +279,7 @@ define([ "implementation": TransactionService, "depends": [ "$q", - "dirtyModelCache" - ] - }, - { - "type": "provider", - "provides": "dirtyModelCache", - "implementation": DirtyModelCache, - "depends": [ - "topic" + "$log" ] } ], @@ -324,8 +312,7 @@ define([ "description": "Provides transactional editing capabilities", "implementation": EditorCapability, "depends": [ - "transactionService", - "dirtyModelCache" + "transactionService" ] } ], diff --git a/platform/commonUI/edit/src/actions/CancelAction.js b/platform/commonUI/edit/src/actions/CancelAction.js index 550516360a..77013ee8f5 100644 --- a/platform/commonUI/edit/src/actions/CancelAction.js +++ b/platform/commonUI/edit/src/actions/CancelAction.js @@ -46,10 +46,19 @@ define( function returnToBrowse () { var parent; - domainObject.getCapability("location").getOriginal().then(function (original) { - parent = original.getCapability("context").getParent(); - parent.getCapability("action").perform("navigate"); - }); + + //If the object existed already, navigate to refresh view + // with previous object state. + if (domainObject.getModel().persisted) { + domainObject.getCapability("action").perform("navigate"); + } else { + //If the object was new, and user has cancelled, then + //navigate back to parent because nothing to show. + domainObject.getCapability("location").getOriginal().then(function (original) { + parent = original.getCapability("context").getParent(); + parent.getCapability("action").perform("navigate"); + }); + } } return this.domainObject.getCapability("editor").cancel() .then(returnToBrowse); @@ -64,7 +73,8 @@ define( CancelAction.appliesTo = function (context) { var domainObject = (context || {}).domainObject; return domainObject !== undefined && - domainObject.getCapability("status").get("editing"); + domainObject.hasCapability('editor') && + domainObject.getCapability('editor').isEditContextRoot(); }; return CancelAction; diff --git a/platform/commonUI/edit/src/actions/EditAction.js b/platform/commonUI/edit/src/actions/EditAction.js index 229861d1d9..acb83c0407 100644 --- a/platform/commonUI/edit/src/actions/EditAction.js +++ b/platform/commonUI/edit/src/actions/EditAction.js @@ -44,7 +44,7 @@ define( * @constructor * @implements {Action} */ - function EditAction($location, navigationService, $log, $q, context) { + function EditAction($location, navigationService, $log, context) { var domainObject = (context || {}).domainObject; // We cannot enter Edit mode if we have no domain object to @@ -63,7 +63,6 @@ define( this.domainObject = domainObject; this.$location = $location; this.navigationService = navigationService; - this.$q = $q; } /** @@ -89,8 +88,11 @@ define( var domainObject = (context || {}).domainObject, type = domainObject && domainObject.getCapability('type'); - // Only allow creatable types to be edited - return type && type.hasFeature('creation') && !domainObject.getCapability('status').get('editing'); + // Only allow editing of types that support it and are not already + // being edited + return type && type.hasFeature('creation') && + domainObject.hasCapability('editor') && + !domainObject.getCapability('editor').isEditContextRoot(); }; return EditAction; diff --git a/platform/commonUI/edit/src/actions/SaveAction.js b/platform/commonUI/edit/src/actions/SaveAction.js index fbd5e51e3f..3879685b9f 100644 --- a/platform/commonUI/edit/src/actions/SaveAction.js +++ b/platform/commonUI/edit/src/actions/SaveAction.js @@ -85,8 +85,9 @@ define( SaveAction.appliesTo = function (context) { var domainObject = (context || {}).domainObject; return domainObject !== undefined && - domainObject.getModel().persisted !== undefined && - domainObject.getCapability("status").get("editing"); + domainObject.hasCapability('editor') && + domainObject.getCapability('editor').isEditContextRoot() && + domainObject.getModel().persisted !== undefined; }; return SaveAction; diff --git a/platform/commonUI/edit/src/actions/SaveAsAction.js b/platform/commonUI/edit/src/actions/SaveAsAction.js index 2f609cfd42..f347cad899 100644 --- a/platform/commonUI/edit/src/actions/SaveAsAction.js +++ b/platform/commonUI/edit/src/actions/SaveAsAction.js @@ -135,8 +135,8 @@ define( return copyService.perform(domainObject, parent, allowClone); } - function cancelEditingAfterClone(clonedObject) { - return domainObject.getCapability("editor").cancel() + function commitEditingAfterClone(clonedObject) { + return domainObject.getCapability("editor").save() .then(resolveWith(clonedObject)); } @@ -144,7 +144,7 @@ define( .then(doWizardSave) .then(getParent) .then(cloneIntoParent) - .then(cancelEditingAfterClone) + .then(commitEditingAfterClone) .catch(resolveWith(false)); }; @@ -157,7 +157,8 @@ define( SaveAsAction.appliesTo = function (context) { var domainObject = (context || {}).domainObject; return domainObject !== undefined && - domainObject.getCapability("status").get("editing") && + domainObject.hasCapability('editor') && + domainObject.getCapability('editor').isEditContextRoot() && domainObject.getModel().persisted === undefined; }; diff --git a/platform/commonUI/edit/src/capabilities/EditorCapability.js b/platform/commonUI/edit/src/capabilities/EditorCapability.js index 2091f3b9b3..df305de73c 100644 --- a/platform/commonUI/edit/src/capabilities/EditorCapability.js +++ b/platform/commonUI/edit/src/capabilities/EditorCapability.js @@ -24,24 +24,42 @@ define( [], function () { + /** + * A capability that implements an editing 'session' for a domain + * object. An editing session is initiated via a call to .edit(). + * Once initiated, any persist operations will be queued pending a + * subsequent call to [.save()](@link #save) or [.cancel()](@link + * #cancel). + * @param transactionService + * @param domainObject + * @constructor + */ function EditorCapability( transactionService, - dirtyModelCache, domainObject ) { this.transactionService = transactionService; - this.dirtyModelCache = dirtyModelCache; this.domainObject = domainObject; } + /** + * Initiate an editing session. This will start a transaction during + * which any persist operations will be deferred until either save() + * or cancel() are called. + */ EditorCapability.prototype.edit = function () { this.transactionService.startTransaction(); this.domainObject.getCapability('status').set('editing', true); }; + function isEditContextRoot (domainObject) { + return domainObject.getCapability('status').get('editing'); + } + function isEditing (domainObject) { - return domainObject.getCapability('status').get('editing') || - domainObject.hasCapability('context') && isEditing(domainObject.getCapability('context').getParent()); + return isEditContextRoot(domainObject) || + domainObject.hasCapability('context') && + isEditing(domainObject.getCapability('context').getParent()); } /** @@ -53,6 +71,20 @@ define( return isEditing(this.domainObject); }; + /** + * Is this the root editing object (ie. the object that the user + * clicked 'edit' on)? + * @returns {*} + */ + EditorCapability.prototype.isEditContextRoot = function () { + return isEditContextRoot(this.domainObject); + }; + + /** + * Save any changes from this editing session. This will flush all + * pending persists and end the current transaction + * @returns {*} + */ EditorCapability.prototype.save = function () { var domainObject = this.domainObject; return this.transactionService.commit().then(function() { @@ -62,6 +94,11 @@ define( EditorCapability.prototype.invoke = EditorCapability.prototype.edit; + /** + * Cancel the current editing session. This will discard any pending + * persist operations + * @returns {*} + */ EditorCapability.prototype.cancel = function () { var domainObject = this.domainObject; return this.transactionService.cancel().then(function(){ @@ -70,15 +107,14 @@ define( }); }; + /** + * @returns {boolean} true if there have been any domain model + * modifications since the last persist, false otherwise. + */ EditorCapability.prototype.dirty = function () { - return this.dirtyModelCache.isDirty(this.domainObject); + return (this.domainObject.getModel().modified || 0) > (this.domainObject.getModel().persisted || 0); }; - EditorCapability.prototype.appliesTo = function(context) { - var domainObject = context.domainObject; - return domainObject && domainObject.getType().hasFeature("creation"); - } - return EditorCapability; } ); diff --git a/platform/commonUI/edit/src/capabilities/TransactionDecorator.js b/platform/commonUI/edit/src/capabilities/TransactionCapabilityDecorator.js similarity index 80% rename from platform/commonUI/edit/src/capabilities/TransactionDecorator.js rename to platform/commonUI/edit/src/capabilities/TransactionCapabilityDecorator.js index 6121ed6faa..44d2b1ed53 100644 --- a/platform/commonUI/edit/src/capabilities/TransactionDecorator.js +++ b/platform/commonUI/edit/src/capabilities/TransactionCapabilityDecorator.js @@ -26,23 +26,30 @@ define( function (TransactionalPersistenceCapability) { 'use strict'; - function TransactionDecorator( + /** + * Wraps the [PersistenceCapability]{@link PersistenceCapability} with + * transactional capabilities. + * @param $q + * @param transactionService + * @param capabilityService + * @see TransactionalPersistenceCapability + * @constructor + */ + function TransactionCapabilityDecorator( $q, transactionService, - dirtyModelCache, capabilityService ) { this.capabilityService = capabilityService; this.transactionService = transactionService; - this.dirtyModelCache = dirtyModelCache; this.$q = $q; } /** - * Decorate PersistenceCapability to ignore persistence calls when a + * Decorate PersistenceCapability to queue persistence calls when a * transaction is in progress. */ - TransactionDecorator.prototype.getCapabilities = function (model) { + TransactionCapabilityDecorator.prototype.getCapabilities = function (model) { var self = this, capabilities = this.capabilityService.getCapabilities(model), persistenceCapability = capabilities.persistence; @@ -55,7 +62,6 @@ define( return new TransactionalPersistenceCapability( self.$q, self.transactionService, - self.dirtyModelCache, original, domainObject ); @@ -63,6 +69,6 @@ define( return capabilities; }; - return TransactionDecorator; + return TransactionCapabilityDecorator; } ); diff --git a/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js b/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js index b282c40e1f..0c6420d484 100644 --- a/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js +++ b/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js @@ -26,44 +26,51 @@ define( function () { 'use strict'; + /** + * Wraps persistence capability to enable transactions. Transactions + * will cause persist calls not to be invoked immediately, but + * rather queued until [EditorCapability.save()]{@link EditorCapability#save} + * or [EditorCapability.cancel()]{@link EditorCapability#cancel} are + * called. + * @memberof platform/commonUI/edit/capabilities + * @param $q + * @param transactionService + * @param persistenceCapability + * @param domainObject + * @constructor + */ function TransactionalPersistenceCapability( $q, transactionService, - dirtyModelCache, persistenceCapability, domainObject ) { this.transactionService = transactionService; - this.dirtyModelCache = dirtyModelCache; - this.persistenceCapability = Object.create(persistenceCapability); + this.persistenceCapability = persistenceCapability; this.domainObject = domainObject; this.$q = $q; } + /** + * The wrapped persist function. If a transaction is active, persist + * will be queued until the transaction is committed or cancelled. + * @returns {*} + */ TransactionalPersistenceCapability.prototype.persist = function () { - var domainObject = this.domainObject, - dirtyModelCache = this.dirtyModelCache; - if (this.transactionService.isActive() && !this.transactionService.isCommitting()) { - dirtyModelCache.markDirty(domainObject); - //Using $q here because need to return something - // from which 'catch' can be chained + if (this.transactionService.isActive()) { + this.transactionService.addToTransaction( + this.persistenceCapability.persist.bind(this.persistenceCapability), + this.persistenceCapability.refresh.bind(this.persistenceCapability) + ); + //Need to return a promise from this function return this.$q.when(true); } else { - return this.persistenceCapability.persist().then(function (result) { - dirtyModelCache.markClean(domainObject); - return result; - }); + return this.persistenceCapability.persist(); } }; TransactionalPersistenceCapability.prototype.refresh = function () { - var domainObject = this.domainObject, - dirtyModelCache = this.dirtyModelCache; - - return this.persistenceCapability.refresh().then(function (result) { - dirtyModelCache.markClean(domainObject); - return result; - }); + return this.persistenceCapability.refresh(); }; TransactionalPersistenceCapability.prototype.getSpace = function () { diff --git a/platform/commonUI/edit/src/policies/EditActionPolicy.js b/platform/commonUI/edit/src/policies/EditActionPolicy.js index 2952758840..78f2c54cb0 100644 --- a/platform/commonUI/edit/src/policies/EditActionPolicy.js +++ b/platform/commonUI/edit/src/policies/EditActionPolicy.js @@ -73,7 +73,8 @@ define( function isEditing(context) { var domainObject = (context || {}).domainObject; return domainObject - && domainObject.getCapability('status').get('editing'); + && domainObject.hasCapability('editor') + && domainObject.getCapability('editor').isEditContextRoot(); } EditActionPolicy.prototype.allow = function (action, context) { diff --git a/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js b/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js index b5853bd89c..afccf5a121 100644 --- a/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js +++ b/platform/commonUI/edit/src/policies/EditContextualActionPolicy.js @@ -34,6 +34,11 @@ define( * from context menu of non-editable objects, when navigated object * is being edited * @constructor + * @param navigationService + * @param editModeBlacklist A blacklist of actions disallowed from + * context menu when navigated object is being edited + * @param nonEditContextBlacklist A blacklist of actions disallowed + * from context menu of non-editable objects, when navigated object * @implements {Policy.} */ function EditContextualActionPolicy(navigationService, editModeBlacklist, nonEditContextBlacklist) { @@ -51,7 +56,7 @@ define( navigatedObject = this.navigationService.getNavigation(), actionMetadata = action.getMetadata ? action.getMetadata() : {}; - if (navigatedObject.getCapability("status").get("editing")) { + if (navigatedObject.hasCapability("editor") && navigatedObject.getCapability("editor").isEditContextRoot()) { if (selectedObject.hasCapability("editor") && selectedObject.getCapability("editor").inEditContext()){ //Target is within the editing context return this.editBlacklist.indexOf(actionMetadata.key) === -1; diff --git a/platform/commonUI/edit/src/policies/EditNavigationPolicy.js b/platform/commonUI/edit/src/policies/EditNavigationPolicy.js index 62c489b35d..83563b5ced 100644 --- a/platform/commonUI/edit/src/policies/EditNavigationPolicy.js +++ b/platform/commonUI/edit/src/policies/EditNavigationPolicy.js @@ -41,12 +41,11 @@ define( EditNavigationPolicy.prototype.isDirty = function(domainObject) { var navigatedObject = domainObject, editorCapability = navigatedObject && - navigatedObject.getCapability("editor"), - statusCapability = navigatedObject && - navigatedObject.getCapability("status"); + navigatedObject.getCapability("editor"); - return statusCapability && statusCapability.get('editing') && - editorCapability && editorCapability.dirty(); + return editorCapability && + editorCapability.isEditContextRoot() && + editorCapability.dirty(); }; /** diff --git a/platform/commonUI/edit/src/policies/EditableMovePolicy.js b/platform/commonUI/edit/src/policies/EditableMovePolicy.js index 4974e0c60d..bb36c86746 100644 --- a/platform/commonUI/edit/src/policies/EditableMovePolicy.js +++ b/platform/commonUI/edit/src/policies/EditableMovePolicy.js @@ -35,10 +35,13 @@ define([], function () { EditableMovePolicy.prototype.allow = function (action, context) { var domainObject = context.domainObject, selectedObject = context.selectedObject, - key = action.getMetadata().key; + key = action.getMetadata().key, + isDomainObjectEditing = domainObject.hasCapability('editor') && + domainObject.getCapability('editor').inEditContext(); - if (key === 'move' && domainObject.hasCapability('editor') && domainObject.getCapability('editor').inEditContext()) { - return !!selectedObject && selectedObject.hasCapability('editor') && selectedObject.getCapability('editor').inEditContext(); + if (key === 'move' && isDomainObjectEditing) { + return !!selectedObject && selectedObject.hasCapability('editor') && + selectedObject.getCapability('editor').inEditContext(); } // Like all policies, allow by default. diff --git a/platform/commonUI/edit/src/representers/EditRepresenter.js b/platform/commonUI/edit/src/representers/EditRepresenter.js index 270f9ecc52..e853669eff 100644 --- a/platform/commonUI/edit/src/representers/EditRepresenter.js +++ b/platform/commonUI/edit/src/representers/EditRepresenter.js @@ -136,7 +136,7 @@ define( } }); - if (representedObject.getCapability('status').get('editing')){ + if (representedObject.hasCapability('editor') && representedObject.getCapability('editor').isEditContextRoot()){ setEditing(); } }; diff --git a/platform/commonUI/edit/src/services/DirtyModelCache.js b/platform/commonUI/edit/src/services/DirtyModelCache.js deleted file mode 100644 index 0439efc6fc..0000000000 --- a/platform/commonUI/edit/src/services/DirtyModelCache.js +++ /dev/null @@ -1,47 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT Web includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ -/*global define*/ -define( - [], - function() { - function DirtyModelCache(topic) { - this.cache = {}; - } - - DirtyModelCache.prototype.get = function () { - return this.cache; - }; - - DirtyModelCache.prototype.isDirty = function (domainObject) { - return !!this.cache[domainObject.getId()]; - }; - - DirtyModelCache.prototype.markDirty = function (domainObject) { - this.cache[domainObject.getId()] = domainObject; - }; - - DirtyModelCache.prototype.markClean = function (domainObject) { - delete this.cache[domainObject.getId()]; - }; - - return DirtyModelCache; - }); \ No newline at end of file diff --git a/platform/commonUI/edit/src/services/TransactionService.js b/platform/commonUI/edit/src/services/TransactionService.js index 2944d2215c..6affde28d8 100644 --- a/platform/commonUI/edit/src/services/TransactionService.js +++ b/platform/commonUI/edit/src/services/TransactionService.js @@ -25,64 +25,90 @@ define( function() { /** * Implements an application-wide transaction state. Once a - * transaction is started, calls to PersistenceCapability.persist() + * transaction is started, calls to + * [PersistenceCapability.persist()]{@link PersistenceCapability#persist} * will be deferred until a subsequent call to - * TransactionService.commit() is made. + * [TransactionService.commit]{@link TransactionService#commit} is made. * + * @memberof platform/commonUI/edit/services * @param $q * @constructor */ - function TransactionService($q, dirtyModelCache) { + function TransactionService($q, $log) { this.$q = $q; + this.$log = $log; this.transaction = false; - this.committing = false; - this.cache = dirtyModelCache; + + this.onCommits = []; + this.onCancels = []; } + /** + * Starts a transaction. While a transaction is active all calls to + * [PersistenceCapability.persist](@link PersistenceCapability#persist) + * will be queued until [commit]{@link #commit} or [cancel]{@link + * #cancel} are called + */ TransactionService.prototype.startTransaction = function () { - if (this.transaction) - console.error("Transaction already in progress") + if (this.transaction) { + //Log error because this is a programming error if it occurs. + this.$log.error("Transaction already in progress"); + } this.transaction = true; }; + /** + * @returns {boolean} If true, indicates that a transaction is in progress + */ TransactionService.prototype.isActive = function () { return this.transaction; }; - TransactionService.prototype.isCommitting = function () { - return this.committing; + /** + * Adds provided functions to a queue to be called on + * [.commit()]{@link #commit} or + * [.cancel()]{@link #commit} + * @param onCommit A function to call on commit + * @param onCancel A function to call on cancel + */ + TransactionService.prototype.addToTransaction = function (onCommit, onCancel) { + if (this.transaction) { + this.onCommits.push(onCommit); + if (onCancel) { + this.onCancels.push(onCancel); + } + } else { + //Log error because this is a programming error if it occurs. + this.$log.error("No transaction in progress"); + } }; /** * All persist calls deferred since the beginning of the transaction - * will be committed. Any failures will be reported via a promise - * rejection. - * @returns {*} + * will be committed. + * + * @returns {Promise} resolved when all persist operations have + * completed. Will reject if any commit operations fail */ TransactionService.prototype.commit = function () { - var self = this; - cache = this.cache.get(); + var self = this, + promises = [], + onCommit; - this.committing = true; - - function keyToObject(key) { - return cache[key]; + while (this.onCommits.length > 0) { // ...using a while in case some onCommit adds to transaction + onCommit = this.onCommits.pop(); + try { // ...also don't want to fail mid-loop... + promises.push(onCommit()); + } catch (e) { + this.$log.error("Error committing transaction."); + } } + return this.$q.all(promises).then( function () { + self.transaction = false; - function objectToPromise(object) { - return object.getCapability('persistence').persist(); - } - - return this.$q.all( - Object.keys(cache) - .map(keyToObject) - .map(objectToPromise)) - .then(function () { - self.transaction = false; - this.committing = false; - }).catch(function() { - return this.committing = false; - }); + self.onCommits = []; + self.onCancels = []; + }); }; /** @@ -95,23 +121,23 @@ define( */ TransactionService.prototype.cancel = function () { var self = this, - cache = this.cache.get(); + results = [], + onCancel; - function keyToObject(key) { - return cache[key]; + while (this.onCancels.length > 0) { + onCancel = this.onCancels.pop(); + try { + results.push(onCancel()); + } catch (error) { + this.$log.error("Error committing transaction."); + } } + return this.$q.all(results).then(function () { + self.transaction = false; - function objectToPromise(object) { - return self.$q.when(object.getModel().persisted && object.getCapability('persistence').refresh()); - } - - return this.$q.all(Object.keys(cache) - .map(keyToObject) - .map(objectToPromise)) - .then(function () { - self.transaction = false; - this.committing = false; - }); + self.onCommits = []; + self.onCancels = []; + }); }; return TransactionService; diff --git a/platform/commonUI/edit/test/actions/EditActionSpec.js b/platform/commonUI/edit/test/actions/EditActionSpec.js index 7ed8b672bd..f645e1cd08 100644 --- a/platform/commonUI/edit/test/actions/EditActionSpec.js +++ b/platform/commonUI/edit/test/actions/EditActionSpec.js @@ -30,7 +30,9 @@ define( mockLog, mockDomainObject, mockType, + mockEditor, actionContext, + capabilities, action; beforeEach(function () { @@ -40,7 +42,7 @@ define( ); mockNavigationService = jasmine.createSpyObj( "navigationService", - [ "setNavigation", "getNavigation" ] + [ "setNavigation", "getNavigation", "addListener", "removeListener" ] ); mockLog = jasmine.createSpyObj( "$log", @@ -48,14 +50,26 @@ define( ); mockDomainObject = jasmine.createSpyObj( "domainObject", - [ "getId", "getModel", "getCapability" ] + [ "getId", "getModel", "getCapability", "hasCapability", "useCapability" ] ); mockType = jasmine.createSpyObj( "type", [ "hasFeature" ] ); + mockEditor = jasmine.createSpyObj( + "editorCapability", + ["edit", "isEditContextRoot", "cancel"] + ); - mockDomainObject.getCapability.andReturn(mockType); + capabilities = { + type: mockType, + editor: mockEditor + }; + + mockDomainObject.getCapability.andCallFake( function (name) { + return capabilities[name]; + }); + mockDomainObject.hasCapability.andReturn(true); mockType.hasFeature.andReturn(true); actionContext = { domainObject: mockDomainObject }; @@ -68,51 +82,34 @@ define( ); }); - it("is only applicable when a domain object is present", function () { + it("is only applicable when an editable domain object is present", function () { expect(EditAction.appliesTo(actionContext)).toBeTruthy(); expect(EditAction.appliesTo({})).toBeFalsy(); + + expect(mockDomainObject.hasCapability).toHaveBeenCalledWith('editor'); // Should have checked for creatability expect(mockType.hasFeature).toHaveBeenCalledWith('creation'); }); - //TODO: Disabled for NEM Beta - xit("changes URL path to edit mode when performed", function () { + it("is only applicable to objects not already in edit mode", function () { + mockEditor.isEditContextRoot.andReturn(false); + expect(EditAction.appliesTo(actionContext)).toBe(true); + mockEditor.isEditContextRoot.andReturn(true); + expect(EditAction.appliesTo(actionContext)).toBe(false); + }); + + it ("cancels editing when user navigates away", function () { action.perform(); - expect(mockLocation.path).toHaveBeenCalledWith("/edit"); + expect(mockNavigationService.addListener).toHaveBeenCalled(); + mockNavigationService.addListener.mostRecentCall.args[0](); + expect(mockEditor.cancel).toHaveBeenCalled(); }); - //TODO: Disabled for NEM Beta - xit("ensures that the edited object is navigated-to", function () { + it ("invokes the Edit capability on the object", function () { action.perform(); - expect(mockNavigationService.setNavigation) - .toHaveBeenCalledWith(mockDomainObject); + expect(mockDomainObject.useCapability).toHaveBeenCalledWith("editor"); }); - //TODO: Disabled for NEM Beta - xit("logs a warning if constructed when inapplicable", function () { - // Verify precondition (ensure warn wasn't called during setup) - expect(mockLog.warn).not.toHaveBeenCalled(); - - // Should not have hit an exception... - new EditAction( - mockLocation, - mockNavigationService, - mockLog, - {} - ).perform(); - - // ...but should have logged a warning - expect(mockLog.warn).toHaveBeenCalled(); - - // And should not have had other interactions - expect(mockLocation.path) - .not.toHaveBeenCalled(); - expect(mockNavigationService.setNavigation) - .not.toHaveBeenCalled(); - }); - - - }); } ); \ No newline at end of file diff --git a/platform/commonUI/edit/test/actions/SaveActionSpec.js b/platform/commonUI/edit/test/actions/SaveActionSpec.js index 1c94bdfe60..09df5e53b5 100644 --- a/platform/commonUI/edit/test/actions/SaveActionSpec.js +++ b/platform/commonUI/edit/test/actions/SaveActionSpec.js @@ -52,7 +52,7 @@ define( ); mockEditorCapability = jasmine.createSpyObj( "editor", - [ "save", "cancel" ] + [ "save", "cancel", "isEditContextRoot" ] ); mockActionCapability = jasmine.createSpyObj( "actionCapability", @@ -71,7 +71,7 @@ define( }); mockDomainObject.getModel.andReturn({persisted: 0}); mockEditorCapability.save.andReturn(mockPromise(true)); - mockDomainObject.getOriginalObject.andReturn(mockDomainObject); + mockEditorCapability.isEditContextRoot.andReturn(true); action = new SaveAction(actionContext); @@ -97,6 +97,13 @@ define( action.perform(); expect(mockEditorCapability.save).toHaveBeenCalled(); }); + + it("navigates to the object after saving", + function () { + action.perform(); + expect(mockActionCapability.perform).toHaveBeenCalledWith("navigate"); + }); + }); } ); \ No newline at end of file diff --git a/platform/commonUI/edit/test/actions/SaveAsActionSpec.js b/platform/commonUI/edit/test/actions/SaveAsActionSpec.js index 47b8440353..bac173ddcc 100644 --- a/platform/commonUI/edit/test/actions/SaveAsActionSpec.js +++ b/platform/commonUI/edit/test/actions/SaveAsActionSpec.js @@ -78,10 +78,11 @@ define( mockEditorCapability = jasmine.createSpyObj( "editor", - [ "save", "cancel" ] + [ "save", "cancel", "isEditContextRoot" ] ); mockEditorCapability.cancel.andReturn(mockPromise(undefined)); mockEditorCapability.save.andReturn(mockPromise(true)); + mockEditorCapability.isEditContextRoot.andReturn(true); capabilities.editor = mockEditorCapability; mockActionCapability = jasmine.createSpyObj( diff --git a/platform/commonUI/edit/test/capabilities/EditorCapabilitySpec.js b/platform/commonUI/edit/test/capabilities/EditorCapabilitySpec.js index d18cdcd931..29b08a4038 100644 --- a/platform/commonUI/edit/test/capabilities/EditorCapabilitySpec.js +++ b/platform/commonUI/edit/test/capabilities/EditorCapabilitySpec.js @@ -25,94 +25,150 @@ define( function (EditorCapability) { describe("The editor capability", function () { - var mockPersistence, - mockEditableObject, - mockDomainObject, - mockCache, - mockCallback, - model, + var mockDomainObject, + capabilities, + mockParentObject, + mockTransactionService, + mockStatusCapability, + mockParentStatus, + mockContextCapability, capability; - beforeEach(function () { - mockPersistence = jasmine.createSpyObj( - "persistence", - [ "persist" ] - ); - mockEditableObject = { - getModel: function () { return model; } + function fastPromise(val) { + return { + then: function (callback) { + return callback(val); + } }; + } + + beforeEach(function () { mockDomainObject = jasmine.createSpyObj( "domainObject", - [ "getId", "getModel", "getCapability", "useCapability" ] + ["getId", "getModel", "hasCapability", "getCapability", "useCapability"] ); - mockCache = jasmine.createSpyObj( - "cache", - [ "saveAll", "markClean" ] + mockParentObject = jasmine.createSpyObj( + "domainObject", + ["getId", "getModel", "hasCapability", "getCapability", "useCapability"] ); - mockCallback = jasmine.createSpy("callback"); + mockTransactionService = jasmine.createSpyObj( + "transactionService", + [ + "startTransaction", + "commit", + "cancel" + ] + ); + mockTransactionService.commit.andReturn(fastPromise()); + mockTransactionService.cancel.andReturn(fastPromise()); - mockDomainObject.getCapability.andReturn(mockPersistence); + mockStatusCapability = jasmine.createSpyObj( + "statusCapability", + ["get", "set"] + ); + mockParentStatus = jasmine.createSpyObj( + "statusCapability", + ["get", "set"] + ); + mockContextCapability = jasmine.createSpyObj( + "contextCapability", + ["getParent"] + ); + mockContextCapability.getParent.andReturn(mockParentObject); - model = { someKey: "some value", x: 42 }; + capabilities = { + context: mockContextCapability, + status: mockStatusCapability + }; + + mockDomainObject.hasCapability.andCallFake(function(name) { + return capabilities[name] !== undefined; + }); + + mockDomainObject.getCapability.andCallFake(function (name) { + return capabilities[name]; + }); + + mockParentObject.getCapability.andReturn(mockParentStatus); + mockParentObject.hasCapability.andReturn(false); capability = new EditorCapability( - mockPersistence, - mockEditableObject, - mockDomainObject, - mockCache + mockTransactionService, + mockDomainObject ); }); - //TODO: Disabled for NEM Beta - xit("mutates the real domain object on nonrecursive save", function () { - capability.save(true).then(mockCallback); + it("starts a transaction when edit is invoked", function () { + capability.edit(); + expect(mockTransactionService.startTransaction).toHaveBeenCalled(); + }); - // Wait for promise to resolve - waitsFor(function () { - return mockCallback.calls.length > 0; - }, 250); + it("sets editing status on object", function () { + capability.edit(); + expect(mockStatusCapability.set).toHaveBeenCalledWith("editing", true); + }); - runs(function () { - expect(mockDomainObject.useCapability) - .toHaveBeenCalledWith("mutation", jasmine.any(Function)); - // We should get the model from the editable object back - expect( - mockDomainObject.useCapability.mostRecentCall.args[1]() - ).toEqual(model); + it("uses editing status to determine editing context root", function () { + capability.edit(); + mockStatusCapability.get.andReturn(false); + expect(capability.isEditContextRoot()).toBe(false); + mockStatusCapability.get.andReturn(true); + expect(capability.isEditContextRoot()).toBe(true); + }); + + it("inEditingContext returns true if parent object is being" + + " edited", function () { + mockStatusCapability.get.andReturn(false); + mockParentStatus.get.andReturn(false); + expect(capability.inEditContext()).toBe(false); + mockParentStatus.get.andReturn(true); + expect(capability.inEditContext()).toBe(true); + }); + + describe("save", function() { + beforeEach(function() { + capability.edit(); + capability.save(); + }); + it("commits the transaction", function () { + expect(mockTransactionService.commit).toHaveBeenCalled(); + }); + it("resets the edit state", function () { + expect(mockStatusCapability.set).toHaveBeenCalledWith('editing', false); }); }); - //TODO: Disabled for NEM Beta - xit("tells the cache to save others", function () { - capability.save().then(mockCallback); - - // Wait for promise to resolve - waitsFor(function () { - return mockCallback.calls.length > 0; - }, 250); - - runs(function () { - expect(mockCache.saveAll).toHaveBeenCalled(); + describe("cancel", function() { + beforeEach(function() { + capability.edit(); + capability.cancel(); + }); + it("cancels the transaction", function () { + expect(mockTransactionService.cancel).toHaveBeenCalled(); + }); + it("resets the edit state", function () { + expect(mockStatusCapability.set).toHaveBeenCalledWith('editing', false); }); }); - //TODO: Disabled for NEM Beta - xit("has no interactions on cancel", function () { - capability.cancel().then(mockCallback); + describe("dirty", function() { + var model = {}; - // Wait for promise to resolve - waitsFor(function () { - return mockCallback.calls.length > 0; - }, 250); + beforeEach(function() { + mockDomainObject.getModel.andReturn(model); + capability.edit(); + capability.cancel(); + }); + it("returns true if the object has been modified since it" + + " was last persisted", function () { + model.modified = 0; + model.persisted = 0; + expect(capability.dirty()).toBe(false); - runs(function () { - expect(mockDomainObject.useCapability).not.toHaveBeenCalled(); - expect(mockCache.markClean).not.toHaveBeenCalled(); - expect(mockCache.saveAll).not.toHaveBeenCalled(); + model.modified = 1; + expect(capability.dirty()).toBe(true); }); }); - - }); } ); \ No newline at end of file diff --git a/platform/commonUI/edit/test/capabilities/TransactionCapabilityDecoratorSpec.js b/platform/commonUI/edit/test/capabilities/TransactionCapabilityDecoratorSpec.js new file mode 100644 index 0000000000..15738167e2 --- /dev/null +++ b/platform/commonUI/edit/test/capabilities/TransactionCapabilityDecoratorSpec.js @@ -0,0 +1,59 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,runs,jasmine,xit,xdescribe*/ + +define( + [ + "../../src/capabilities/TransactionalPersistenceCapability", + "../../src/capabilities/TransactionCapabilityDecorator" + ], + function (TransactionalPersistenceCapability, TransactionCapabilityDecorator) { + "use strict"; + + describe("The transaction capability decorator", function () { + var mockQ, + mockTransactionService, + mockCapabilityService, + provider; + + beforeEach(function() { + //mockQ = jasmine.createSpyObj("$q", []); + mockQ = {}; + //mockTransactionService = + // jasmine.createSpyObj("transactionService", []); + mockTransactionService = {}; + mockCapabilityService = jasmine.createSpyObj("capabilityService", ["getCapabilities"]); + mockCapabilityService.getCapabilities.andReturn({ + persistence: function() {} + }); + + provider = new TransactionCapabilityDecorator(mockQ, mockTransactionService, mockCapabilityService); + + }); + it("decorates the persistence capability", function() { + var capabilities = provider.getCapabilities(); + expect(capabilities.persistence({}) instanceof TransactionalPersistenceCapability).toBe(true); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/commonUI/edit/test/capabilities/TransactionalPersistenceCapabilitySpec.js b/platform/commonUI/edit/test/capabilities/TransactionalPersistenceCapabilitySpec.js new file mode 100644 index 0000000000..baa870934d --- /dev/null +++ b/platform/commonUI/edit/test/capabilities/TransactionalPersistenceCapabilitySpec.js @@ -0,0 +1,92 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,runs,jasmine,xit,xdescribe*/ + +define( + [ + "../../src/capabilities/TransactionalPersistenceCapability" + ], + function (TransactionalPersistenceCapability) { + "use strict"; + + function fastPromise(val) { + return { + then: function(callback) { + return callback(val); + } + }; + } + + describe("The transactional persistence decorator", function () { + var mockQ, + mockTransactionService, + mockPersistence, + mockDomainObject, + capability; + + beforeEach(function() { + mockQ = jasmine.createSpyObj("$q", ["when"]); + mockQ.when.andCallFake(function (val) { + return fastPromise(val); + }); + mockTransactionService = jasmine.createSpyObj( + "transactionService", + ["isActive", "addToTransaction"] + ); + mockPersistence = jasmine.createSpyObj( + "persistenceCapability", + ["persist", "refresh"] + ); + + capability = new TransactionalPersistenceCapability(mockQ, mockTransactionService, mockPersistence, mockDomainObject); + }); + + it("if no transaction is active, passes through to persistence" + + " provider", function() { + mockTransactionService.isActive.andReturn(false); + capability.persist(); + expect(mockPersistence.persist).toHaveBeenCalled(); + }); + + it("if transaction is active, persist call is queued", function() { + mockTransactionService.isActive.andReturn(true); + capability.persist(); + expect(mockTransactionService.addToTransaction).toHaveBeenCalled(); + + //Test that it was the persist call that was queued + mockTransactionService.addToTransaction.mostRecentCall.args[0](); + expect(mockPersistence.persist).toHaveBeenCalled(); + }); + + it("if transaction is active, refresh call is queued as cancel" + + " function", function() { + mockTransactionService.isActive.andReturn(true); + capability.persist(); + + //Test that it was the persist call that was queued + mockTransactionService.addToTransaction.mostRecentCall.args[1](); + expect(mockPersistence.refresh).toHaveBeenCalled(); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/commonUI/edit/test/policies/EditActionPolicySpec.js b/platform/commonUI/edit/test/policies/EditActionPolicySpec.js index dfe4e8fc4c..0cdf1be85d 100644 --- a/platform/commonUI/edit/test/policies/EditActionPolicySpec.js +++ b/platform/commonUI/edit/test/policies/EditActionPolicySpec.js @@ -34,7 +34,7 @@ define( mockEditAction, mockPropertiesAction, mockTypeCapability, - mockStatusCapability, + mockEditorCapability, capabilities, plotView, policy; @@ -48,11 +48,10 @@ define( 'getCapability' ] ); - mockStatusCapability = jasmine.createSpyObj('statusCapability', ['get']); - mockStatusCapability.get.andReturn(false); + mockEditorCapability = jasmine.createSpyObj('editorCapability', ['isEditContextRoot']); mockTypeCapability = jasmine.createSpyObj('type', ['getKey']); capabilities = { - 'status': mockStatusCapability, + 'editor': mockEditorCapability, 'type': mockTypeCapability }; @@ -112,7 +111,7 @@ define( it("disallows the edit action when object is already being" + " edited", function () { testViews = [ editableView ]; - mockStatusCapability.get.andReturn(true); + mockEditorCapability.isEditContextRoot.andReturn(true); expect(policy.allow(mockEditAction, testContext)).toBe(false); }); diff --git a/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js b/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js index c1c9878e6e..a516061e80 100644 --- a/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js +++ b/platform/commonUI/edit/test/policies/EditContextualActionPolicySpec.js @@ -32,16 +32,22 @@ define( context, navigatedObject, mockDomainObject, + mockEditorCapability, metadata, editModeBlacklist = ["copy", "follow", "window", "link", "locate"], nonEditContextBlacklist = ["copy", "follow", "properties", "move", "link", "remove", "locate"]; beforeEach(function () { - navigatedObject = jasmine.createSpyObj("navigatedObject", ["hasCapability"]); + mockEditorCapability = jasmine.createSpyObj("editorCapability", ["isEditContextRoot", "inEditContext"]); + + navigatedObject = jasmine.createSpyObj("navigatedObject", ["hasCapability", "getCapability"]); + navigatedObject.getCapability.andReturn(mockEditorCapability); navigatedObject.hasCapability.andReturn(false); + mockDomainObject = jasmine.createSpyObj("domainObject", ["hasCapability", "getCapability"]); mockDomainObject.hasCapability.andReturn(false); + mockDomainObject.getCapability.andReturn(mockEditorCapability); navigationService = jasmine.createSpyObj("navigationService", ["getNavigation"]); navigationService.getNavigation.andReturn(navigatedObject); @@ -62,6 +68,7 @@ define( it('Allows "window" action when navigated object in edit mode,' + ' but selected object not in edit mode ', function() { navigatedObject.hasCapability.andReturn(true); + mockEditorCapability.isEditContextRoot.andReturn(true); metadata.key = "window"; expect(policy.allow(mockAction, context)).toBe(true); }); @@ -91,6 +98,8 @@ define( it('Disallows "move" action when navigated object in edit mode,' + ' but selected object not in edit mode ', function() { navigatedObject.hasCapability.andReturn(true); + mockEditorCapability.isEditContextRoot.andReturn(true); + mockEditorCapability.inEditContext.andReturn(false); metadata.key = "move"; expect(policy.allow(mockAction, context)).toBe(false); }); @@ -99,6 +108,9 @@ define( ' selected object in edit mode', function() { navigatedObject.hasCapability.andReturn(true); mockDomainObject.hasCapability.andReturn(true); + mockEditorCapability.isEditContextRoot.andReturn(true); + mockEditorCapability.inEditContext.andReturn(true); + metadata.key = "copy"; expect(policy.allow(mockAction, context)).toBe(false); }); diff --git a/platform/commonUI/edit/test/policies/EditableViewPolicySpec.js b/platform/commonUI/edit/test/policies/EditableViewPolicySpec.js index 2194a8c45a..32400a7453 100644 --- a/platform/commonUI/edit/test/policies/EditableViewPolicySpec.js +++ b/platform/commonUI/edit/test/policies/EditableViewPolicySpec.js @@ -33,8 +33,13 @@ define( testMode = true; // Act as if we're in Edit mode by default mockDomainObject = jasmine.createSpyObj( 'domainObject', - ['hasCapability'] + ['hasCapability', 'getCapability'] ); + mockDomainObject.getCapability.andReturn({ + inEditContext: function () { + return true; + } + }); mockDomainObject.hasCapability.andCallFake(function (c) { return (c === 'editor') && testMode; }); diff --git a/platform/commonUI/edit/test/representers/EditRepresenterSpec.js b/platform/commonUI/edit/test/representers/EditRepresenterSpec.js index b4c2f4ce7f..fe0c17c5bd 100644 --- a/platform/commonUI/edit/test/representers/EditRepresenterSpec.js +++ b/platform/commonUI/edit/test/representers/EditRepresenterSpec.js @@ -32,6 +32,7 @@ define( mockDomainObject, mockPersistence, mockStatusCapability, + mockEditorCapability, mockCapabilities, representer; @@ -58,11 +59,14 @@ define( mockPersistence = jasmine.createSpyObj("persistence", ["persist"]); mockStatusCapability = - jasmine.createSpyObj("statusCapability", ["get", "listen"]); - mockStatusCapability.get.andReturn(false); + jasmine.createSpyObj("statusCapability", ["listen"]); + mockEditorCapability = + jasmine.createSpyObj("editorCapability", ["isEditContextRoot"]); + mockCapabilities = { 'persistence': mockPersistence, - 'status': mockStatusCapability + 'status': mockStatusCapability, + 'editor': mockEditorCapability }; mockDomainObject.getModel.andReturn({}); @@ -82,6 +86,7 @@ define( it("Sets edit view template on edit mode", function () { mockStatusCapability.listen.mostRecentCall.args[0](['editing']); + mockEditorCapability.isEditContextRoot.andReturn(true); expect(mockScope.viewObjectTemplate).toEqual('edit-object'); }); diff --git a/platform/commonUI/edit/test/services/TransactionServiceSpec.js b/platform/commonUI/edit/test/services/TransactionServiceSpec.js new file mode 100644 index 0000000000..6c057aa9bd --- /dev/null +++ b/platform/commonUI/edit/test/services/TransactionServiceSpec.js @@ -0,0 +1,127 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,jasmine*/ + +define( + ["../../src/services/TransactionService"], + function (TransactionService) { + "use strict"; + + describe("The Transaction Service", function () { + var mockQ, + mockLog, + transactionService; + + function fastPromise (val) { + return { + then: function (callback) { + return fastPromise(callback(val)); + } + }; + } + + beforeEach(function () { + mockQ = jasmine.createSpyObj("$q", ["all"]); + mockQ.all.andReturn(fastPromise()); + mockLog = jasmine.createSpyObj("$log", ["error"]); + transactionService = new TransactionService(mockQ, mockLog); + }); + + it("isActive returns true if a transaction is in progress", function () { + expect(transactionService.isActive()).toBe(false); + transactionService.startTransaction(); + expect(transactionService.isActive()).toBe(true); + }); + + it("addToTransaction queues onCommit and onCancel functions", function () { + var onCommit = jasmine.createSpy('onCommit'), + onCancel = jasmine.createSpy('onCancel'); + + transactionService.startTransaction(); + transactionService.addToTransaction(onCommit, onCancel); + expect(transactionService.onCommits.length).toBe(1); + expect(transactionService.onCancels.length).toBe(1); + }); + + describe("commit", function () { + var onCommits; + + beforeEach(function() { + onCommits = [0, 1, 2].map(function(val) { + return jasmine.createSpy("onCommit" + val); + }); + + transactionService.startTransaction(); + onCommits.forEach(transactionService.addToTransaction.bind(transactionService)); + }); + + it("commit calls all queued commit functions", function () { + expect(transactionService.onCommits.length).toBe(3); + transactionService.commit(); + onCommits.forEach( function (spy) { + expect(spy).toHaveBeenCalled(); + }); + }); + + it("commit resets active state and clears queues", function () { + transactionService.commit(); + expect(transactionService.isActive()).toBe(false); + expect(transactionService.onCommits.length).toBe(0); + expect(transactionService.onCancels.length).toBe(0); + }); + + }); + + describe("cancel", function () { + var onCancels; + + beforeEach(function() { + onCancels = [0, 1, 2].map(function(val) { + return jasmine.createSpy("onCancel" + val); + }); + + transactionService.startTransaction(); + onCancels.forEach(function (onCancel) { + transactionService.addToTransaction(undefined, onCancel); + }); + }); + + it("cancel calls all queued cancel functions", function () { + expect(transactionService.onCancels.length).toBe(3); + transactionService.cancel(); + onCancels.forEach( function (spy) { + expect(spy).toHaveBeenCalled(); + }); + }); + + it("cancel resets active state and clears queues", function () { + transactionService.cancel(); + expect(transactionService.isActive()).toBe(false); + expect(transactionService.onCommits.length).toBe(0); + expect(transactionService.onCancels.length).toBe(0); + }); + + }); + + }); + } +); \ No newline at end of file diff --git a/platform/commonUI/regions/src/EditableRegionPolicy.js b/platform/commonUI/regions/src/EditableRegionPolicy.js index 5b787a00d8..a63bed65b1 100644 --- a/platform/commonUI/regions/src/EditableRegionPolicy.js +++ b/platform/commonUI/regions/src/EditableRegionPolicy.js @@ -39,7 +39,7 @@ define( if (!regionPart.modes){ return true; } - if (domainObject.getCapability('status').get('editing')){ + if (domainObject.hasCapability('editor') && domainObject.getCapability('editor').inEditContext()){ //If the domain object is in edit mode, only include a part // if it is marked editable return regionPart.modes.indexOf('edit') !== -1; diff --git a/platform/commonUI/regions/test/EditableRegionPolicySpec.js b/platform/commonUI/regions/test/EditableRegionPolicySpec.js index 1692f72288..e9a6a97404 100644 --- a/platform/commonUI/regions/test/EditableRegionPolicySpec.js +++ b/platform/commonUI/regions/test/EditableRegionPolicySpec.js @@ -28,7 +28,7 @@ define( var editableRegionPolicy, mockDomainObject, - mockStatusCapability, + mockEditorCapability, mockBrowseRegionPart = { modes: 'browse' }, @@ -40,31 +40,32 @@ define( beforeEach(function(){ editableRegionPolicy = new EditableRegionPolicy(); - mockStatusCapability = jasmine.createSpyObj("statusCapability", [ - "get" + mockEditorCapability = jasmine.createSpyObj("editorCapability", [ + "inEditContext" ]); mockDomainObject = jasmine.createSpyObj("domainObject", [ - "getCapability" + "hasCapability", "getCapability" ]); - mockDomainObject.getCapability.andReturn(mockStatusCapability); + mockDomainObject.hasCapability.andReturn(true); + mockDomainObject.getCapability.andReturn(mockEditorCapability); }); it("includes only browse region parts for object not in edit mode", function() { - mockStatusCapability.get.andReturn(false); + mockEditorCapability.inEditContext.andReturn(false); expect(editableRegionPolicy.allow(mockBrowseRegionPart, mockDomainObject)).toBe(true); expect(editableRegionPolicy.allow(mockEditRegionPart, mockDomainObject)).toBe(false); }); it("includes only edit region parts for object in edit mode", function() { - mockStatusCapability.get.andReturn(true); + mockEditorCapability.inEditContext.andReturn(true); expect(editableRegionPolicy.allow(mockBrowseRegionPart, mockDomainObject)).toBe(false); expect(editableRegionPolicy.allow(mockEditRegionPart, mockDomainObject)).toBe(true); }); it("includes region parts with no mode specification", function() { - mockStatusCapability.get.andReturn(false); + mockEditorCapability.inEditContext.andReturn(false); expect(editableRegionPolicy.allow(mockAllModesRegionPart, mockDomainObject)).toBe(true); - mockStatusCapability.get.andReturn(true); + mockEditorCapability.inEditContext.andReturn(true); expect(editableRegionPolicy.allow(mockAllModesRegionPart, mockDomainObject)).toBe(true); }); diff --git a/platform/core/test/capabilities/PersistenceCapabilitySpec.js b/platform/core/test/capabilities/PersistenceCapabilitySpec.js index 16f5d34e61..d2bafd26e6 100644 --- a/platform/core/test/capabilities/PersistenceCapabilitySpec.js +++ b/platform/core/test/capabilities/PersistenceCapabilitySpec.js @@ -155,18 +155,6 @@ define( expect(model).toEqual(refreshModel); }); - it("does not overwrite unpersisted changes on refresh", function () { - var refreshModel = {someOtherKey: "some other value"}, - mockCallback = jasmine.createSpy(); - model.modified = 2; - model.persisted = 1; - mockPersistenceService.readObject.andReturn(asPromise(refreshModel)); - persistence.refresh().then(mockCallback); - expect(model).not.toEqual(refreshModel); - // Should have also indicated that no changes were actually made - expect(mockCallback).toHaveBeenCalledWith(false); - }); - it("does not trigger error notification on successful" + " persistence", function () { persistence.persist(); diff --git a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js index 620e70e3e1..e655741342 100644 --- a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js +++ b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js @@ -40,7 +40,7 @@ define( } // Check if we are in edit mode (also check parents) - function inEditMode(swimlane) { + function inEditMode() { return swimlane.domainObject.hasCapability('editor') && swimlane.domainObject.getCapability('editor').inEditContext(); } @@ -174,7 +174,7 @@ define( * @returns {boolean} true if this should be allowed */ allowDropIn: function (id, domainObject) { - return inEditMode(swimlane) && + return inEditMode() && !pathContains(swimlane, id) && !contains(swimlane, id) && canDrop(swimlane.domainObject, domainObject); @@ -189,7 +189,7 @@ define( allowDropAfter: function (id, domainObject) { var target = expandedForDropInto() ? swimlane : swimlane.parent; - return inEditMode(swimlane) && + return inEditMode() && target && !pathContains(target, id) && canDrop(target.domainObject, domainObject); diff --git a/platform/features/timeline/test/controllers/TimelineZoomControllerSpec.js b/platform/features/timeline/test/controllers/TimelineZoomControllerSpec.js index 6bc922731d..d7322512d5 100644 --- a/platform/features/timeline/test/controllers/TimelineZoomControllerSpec.js +++ b/platform/features/timeline/test/controllers/TimelineZoomControllerSpec.js @@ -82,11 +82,18 @@ define( it("persists zoom changes in Edit mode", function () { mockScope.domainObject = jasmine.createSpyObj( 'domainObject', - ['hasCapability'] + ['hasCapability', 'getCapability'] ); mockScope.domainObject.hasCapability.andCallFake(function (c) { return c === 'editor'; }); + mockScope.domainObject.getCapability.andCallFake(function (c) { + if (c === 'editor') { + return { + inEditContext: function () {return true;} + }; + } + }); controller.zoom(1); expect(mockScope.commit).toHaveBeenCalled(); expect(mockScope.configuration.zoomLevel) diff --git a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js index 4c8d063f29..3eed051f45 100644 --- a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js +++ b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js @@ -28,6 +28,7 @@ define( var mockSwimlane, mockOtherObject, mockActionCapability, + mockEditorCapability, mockPersistence, mockContext, mockAction, @@ -36,6 +37,8 @@ define( beforeEach(function () { var mockPromise = jasmine.createSpyObj('promise', ['then']); + mockEditorCapability = jasmine.createSpyObj('editorCapability', ['inEditContext']); + mockSwimlane = jasmine.createSpyObj( "swimlane", [ "highlight", "highlightBottom" ] @@ -86,19 +89,22 @@ define( mockSwimlane.domainObject.getCapability.andCallFake(function (c) { return { action: mockActionCapability, - persistence: mockPersistence + persistence: mockPersistence, + editor: mockEditorCapability }[c]; }); mockSwimlane.parent.domainObject.getCapability.andCallFake(function (c) { return { action: mockActionCapability, - persistence: mockPersistence + persistence: mockPersistence, + editor: mockEditorCapability }[c]; }); mockOtherObject.getCapability.andCallFake(function (c) { return { action: mockActionCapability, - context: mockContext + context: mockContext, + editor: mockEditorCapability }[c]; }); mockContext.getParent.andReturn(mockOtherObject); @@ -109,13 +115,14 @@ define( }); it("disallows drop outside of edit mode", function () { + mockEditorCapability.inEditContext.andReturn(true); // Verify precondition expect(handler.allowDropIn('d', mockSwimlane.domainObject)) .toBeTruthy(); expect(handler.allowDropAfter('d', mockSwimlane.domainObject)) .toBeTruthy(); // Act as if we're not in edit mode - mockSwimlane.domainObject.hasCapability.andReturn(false); + mockEditorCapability.inEditContext.andReturn(false); // Now, they should be disallowed expect(handler.allowDropIn('d', mockSwimlane.domainObject)) .toBeFalsy(); diff --git a/platform/representation/src/MCTRepresentation.js b/platform/representation/src/MCTRepresentation.js index 06f7220fd9..bcaa90e4e7 100644 --- a/platform/representation/src/MCTRepresentation.js +++ b/platform/representation/src/MCTRepresentation.js @@ -53,9 +53,7 @@ define( * @param {ViewDefinition[]} views an array of view extensions */ function MCTRepresentation(representations, views, representers, $q, templateLinker, $log) { - var representationMap = {}, - listeners = 0; - domainObjectListener; + var representationMap = {}; // Assemble all representations and views // The distinction between views and representations is @@ -250,10 +248,10 @@ define( } /** - * Add a listener for status changes to the object itself. + * Add a listener to the object for status changes. */ - $scope.$watch("domainObject", function(domainObject, oldDomainObject) { - if (domainObject!==oldDomainObject){ + $scope.$watch("domainObject", function (domainObject, oldDomainObject) { + if (domainObject !== oldDomainObject){ listenForStatusChange(domainObject); } }); diff --git a/platform/representation/src/gestures/DropGesture.js b/platform/representation/src/gestures/DropGesture.js index e323e9d4a2..f276460b53 100644 --- a/platform/representation/src/gestures/DropGesture.js +++ b/platform/representation/src/gestures/DropGesture.js @@ -103,13 +103,18 @@ define( // the change. if (id) { e.preventDefault(); - if (domainObjectType!=='folder') { - domainObject.getCapability('action').perform('edit'); - } + //Use scope.apply, drop event is outside digest cycle + // and if not applied here causes visual artifacts. + scope.$apply( function() { + if (domainObjectType !== 'folder') { + domainObject.getCapability('action').perform('edit'); + } + }); $q.when(action && action.perform()).then(function () { broadcastDrop(id, event); }); + } } diff --git a/platform/representation/test/MCTRepresentationSpec.js b/platform/representation/test/MCTRepresentationSpec.js index 7608070d3e..36372a66c4 100644 --- a/platform/representation/test/MCTRepresentationSpec.js +++ b/platform/representation/test/MCTRepresentationSpec.js @@ -36,6 +36,7 @@ define( testViews, testUrls, mockRepresenters, + mockStatusCapability, mockQ, mockLinker, mockLog, @@ -118,6 +119,8 @@ define( mockChangeTemplate = jasmine.createSpy('changeTemplate'); mockLog = jasmine.createSpyObj("$log", LOG_FUNCTIONS); + mockStatusCapability = jasmine.createSpyObj("statusCapability", ["listen"]); + mockScope = jasmine.createSpyObj("scope", [ "$watch", "$on" ]); mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS); mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS); @@ -128,6 +131,10 @@ define( return testUrls[ext.key]; }); + mockDomainObject.getCapability.andCallFake(function (c) { + return c === 'status' && mockStatusCapability; + }); + mctRepresentation = new MCTRepresentation( testRepresentations, testViews, diff --git a/platform/search/src/services/GenericSearchProvider.js b/platform/search/src/services/GenericSearchProvider.js index fbe45ae6d8..101c718669 100644 --- a/platform/search/src/services/GenericSearchProvider.js +++ b/platform/search/src/services/GenericSearchProvider.js @@ -120,8 +120,8 @@ define([ provider = this; mutationTopic.listen(function (mutatedObject) { - var status = mutatedObject.getCapability('status'); - if (!status || !status.get('editing')) { + var editor = mutatedObject.getCapability('editor'); + if (!editor || !editor.inEditContext()) { provider.index( mutatedObject.getId(), mutatedObject.getModel()