diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 8eac4d5012..b59b19d54c 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -40,6 +40,10 @@ define([ "./src/policies/EditContextualActionPolicy", "./src/representers/EditRepresenter", "./src/representers/EditToolbarRepresenter", + "./src/capabilities/EditorCapability", + "./src/capabilities/TransactionDecorator", + "./src/services/TransactionService", + "./src/services/DirtyModelCache", "text!./res/templates/library.html", "text!./res/templates/edit-object.html", "text!./res/templates/edit-action-buttons.html", @@ -66,6 +70,10 @@ define([ EditContextualActionPolicy, EditRepresenter, EditToolbarRepresenter, + EditorCapability, + TransactionDecorator, + TransactionService, + DirtyModelCache, libraryTemplate, editObjectTemplate, editActionButtonsTemplate, @@ -261,6 +269,35 @@ define([ "template": topbarEditTemplate } ], + "components": [ + { + "type": "decorator", + "provides": "capabilityService", + "implementation": TransactionDecorator, + "depends": [ + "$q", + "transactionService", + "dirtyModelCache" + ] + }, + { + "type": "provider", + "provides": "transactionService", + "implementation": TransactionService, + "depends": [ + "$q", + "dirtyModelCache" + ] + }, + { + "type": "provider", + "provides": "dirtyModelCache", + "implementation": DirtyModelCache, + "depends": [ + "topic" + ] + } + ], "representers": [ { "implementation": EditRepresenter, @@ -282,7 +319,19 @@ define([ "key": "nonEditContextBlacklist", "value": ["copy", "follow", "properties", "move", "link", "remove", "locate"] } - ] + ], + "capabilities": [ + { + "key": "editor", + "name": "Editor Capability", + "description": "Provides transactional editing capabilities", + "implementation": EditorCapability, + "depends": [ + "transactionService", + "dirtyModelCache" + ] + } + ], } }); }); diff --git a/platform/commonUI/edit/src/capabilities/EditorCapability.js b/platform/commonUI/edit/src/capabilities/EditorCapability.js index ff9dfc19ed..6987e02eec 100644 --- a/platform/commonUI/edit/src/capabilities/EditorCapability.js +++ b/platform/commonUI/edit/src/capabilities/EditorCapability.js @@ -24,12 +24,10 @@ define( [], function () { - /** * Implements "save" and "cancel" as capabilities of * the object. In editing mode, user is seeing/using - * a copy of the object (an EditableDomainObject) - * which is disconnected from persistence; the Save + * a copy of the object which is disconnected from persistence; the Save * and Cancel actions can use this capability to * propagate changes from edit mode to the underlying * actual persistable object. @@ -41,99 +39,37 @@ define( * @memberof platform/commonUI/edit */ function EditorCapability( - persistenceCapability, - editableObject, - domainObject, - cache + transactionService, + dirtyModelCache, + domainObject ) { - this.editableObject = editableObject; + this.transactionService = transactionService; + this.dirtyModelCache = dirtyModelCache; this.domainObject = domainObject; - this.cache = cache; } - // Simulate Promise.resolve (or $q.when); the former - // causes a delayed reaction from Angular (since it - // does not trigger a digest) and the latter is not - // readily accessible, since we're a few classes - // removed from the layer which gets dependency - // injection. - function resolvePromise(value) { - return (value && value.then) ? value : { - then: function (callback) { - return resolvePromise(callback(value)); - } - }; - } + EditorCapability.prototype.edit = function () { + this.transactionService.startTransaction(); + this.getCapability('status').set('editing', true); + }; - /** - * Save any changes that have been made to this domain object - * (as well as to others that might have been retrieved and - * modified during the editing session) - * @param {boolean} nonrecursive if true, save only this - * object (and not other objects with associated changes) - * @returns {Promise} a promise that will be fulfilled after - * persistence has completed. - * @memberof platform/commonUI/edit.EditorCapability# - */ - EditorCapability.prototype.save = function (nonrecursive) { - var domainObject = this.domainObject, - editableObject = this.editableObject, - self = this, - cache = this.cache, - returnPromise; + EditorCapability.prototype.save = function () { + return this.transactionService.commit(); + }; - // Update the underlying, "real" domain object's model - // with changes made to the copy used for editing. - function doMutate() { - return domainObject.useCapability('mutation', function () { - return editableObject.getModel(); - }); - } - - // Persist the underlying domain object - function doPersist() { - return domainObject.getCapability('persistence').persist(); - } - - editableObject.getCapability("status").set("editing", false); - - if (nonrecursive) { - returnPromise = resolvePromise(doMutate()) - .then(doPersist) - .then(function(){ - self.cancel(); - }); - } else { - returnPromise = resolvePromise(cache.saveAll()); - } - //Return the original (non-editable) object - return returnPromise.then(function() { - return domainObject.getOriginalObject ? domainObject.getOriginalObject() : domainObject; + EditorCapability.prototype.cancel = function () { + var domainObject = this.domainObject; + return this.transactionService.cancel().then(function(){ + domainObject.getCapability("status").set("editing", false); }); }; - /** - * Cancel editing; Discard any changes that have been made to - * this domain object (as well as to others that might have - * been retrieved and modified during the editing session) - * @returns {Promise} a promise that will be fulfilled after - * cancellation has completed. - * @memberof platform/commonUI/edit.EditorCapability# - */ - EditorCapability.prototype.cancel = function () { - this.editableObject.getCapability("status").set("editing", false); - this.cache.markClean(); - return resolvePromise(undefined); + EditorCapability.prototype.dirty = function () { + return this.dirtyModelCache.isDirty(this.domainObject); }; - /** - * Check if there are any unsaved changes. - * @returns {boolean} true if there are unsaved changes - * @memberof platform/commonUI/edit.EditorCapability# - */ - EditorCapability.prototype.dirty = function () { - return this.cache.dirty(); - }; + //TODO: add 'appliesTo'. EditorCapability should not be available + // for objects that should not be edited return EditorCapability; } diff --git a/platform/commonUI/edit/src/capabilities/TransactionDecorator.js b/platform/commonUI/edit/src/capabilities/TransactionDecorator.js new file mode 100644 index 0000000000..f740dfc37b --- /dev/null +++ b/platform/commonUI/edit/src/capabilities/TransactionDecorator.js @@ -0,0 +1,82 @@ +/***************************************************************************** + * 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( + ['./TransactionalPersistenceCapability'], + function (TransactionalPersistenceCapability) { + 'use strict'; + + /** + * Implements "save" and "cancel" as capabilities of + * the object. In editing mode, user is seeing/using + * a copy of the object (an EditableDomainObject) + * which is disconnected from persistence; the Save + * and Cancel actions can use this capability to + * propagate changes from edit mode to the underlying + * actual persistable object. + * + * Meant specifically for use by EditableDomainObject and the + * associated cache; the constructor signature is particular + * to a pattern used there and may contain unused arguments. + * @constructor + * @memberof platform/commonUI/edit + */ + function TransactionDecorator( + $q, + transactionService, + dirtyModelCache, + capabilityService + ) { + this.capabilityService = capabilityService; + this.transactionService = transactionService; + this.dirtyModelCache = dirtyModelCache; + this.$q = $q; + } + + /** + * Decorate PersistenceCapability to ignore persistence calls when a + * transaction is in progress. + */ + TransactionDecorator.prototype.getCapabilities = function (model) { + var capabilities = this.capabilityService.getCapabilities(model), + persistenceCapability = capabilities.persistence; + + capabilities.persistence = function (domainObject) { + var original = + (typeof persistenceCapability === 'function') ? + persistenceCapability(domainObject) : + persistenceCapability; + return new TransactionalPersistenceCapability( + self.$q, + self.transactionService, + self.dirtyModelCache, + original, + domainObject + ); + }; + return capabilities; + }; + + return TransactionDecorator; + } +); diff --git a/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js b/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js new file mode 100644 index 0000000000..55f7483e63 --- /dev/null +++ b/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js @@ -0,0 +1,73 @@ +/***************************************************************************** + * 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 () { + 'use strict'; + + function TransactionalPersistenceCapability( + $q, + transactionService, + dirtyModelCache, + persistenceCapability, + domainObject + ) { + this.transactionService = transactionService; + this.dirtyModelCache = dirtyModelCache; + this.persistenceCapability = Object.create(persistenceCapability); + this.domainObject = domainObject; + this.$q = $q; + } + + TransactionalPersistenceCapability.prototype.persist = function () { + var domainObject = this.domainObject, + dirtyModelCache = this.dirtyModelCache; + if (this.transactionService.isActive()) { + dirtyModelCache.markDirty(domainObject); + //Using $q here because need to return something + // from which 'catch' can be chained + return this.$q.when(true); + } else { + return this.persistenceCapability.persist().then(function (result) { + dirtyModelCache.markClean(domainObject); + return result; + }); + } + }; + + TransactionalPersistenceCapability.prototype.refresh = function () { + var dirtyModelCache = this.dirtyModelCache; + return this.persistenceCapability.refresh().then(function (result) { + dirtyModelCache.markClean(domainObject); + return result; + }); + }; + + TransactionalPersistenceCapability.prototype.getSpace = function () { + return this.persistenceCapability.getSpace(); + }; + + return TransactionalPersistenceCapability; + } +); diff --git a/platform/commonUI/edit/src/services/DirtyModelCache.js b/platform/commonUI/edit/src/services/DirtyModelCache.js new file mode 100644 index 0000000000..df070ab3ab --- /dev/null +++ b/platform/commonUI/edit/src/services/DirtyModelCache.js @@ -0,0 +1,47 @@ +/***************************************************************************** + * 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.get(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 new file mode 100644 index 0000000000..d156b9ee2e --- /dev/null +++ b/platform/commonUI/edit/src/services/TransactionService.js @@ -0,0 +1,108 @@ +/***************************************************************************** + * 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() { + /** + * Implements an application-wide transaction state. Once a + * transaction is started, calls to PersistenceCapability.persist() + * will be deferred until a subsequent call to + * TransactionService.commit() is made. + * + * @param $q + * @constructor + */ + function TransactionService($q, dirtyModelCache) { + this.$q = $q; + this.transaction = false; + this.cache = dirtyModelCache; + } + + TransactionService.prototype.startTransaction = function () { + if (this.transaction) { + throw "Transaction in progress"; + } + this.transaction = true; + }; + + TransactionService.prototype.isActive = function () { + return this.transaction; + }; + + /** + * All persist calls deferred since the beginning of the transaction + * will be committed. Any failures will be reported via a promise + * rejection. + * @returns {*} + */ + TransactionService.prototype.commit = function () { + var self = this; + cache = this.cache.get(); + + function keyToObject(key) { + return cache[key]; + } + + function objectToPromise(object) { + return object.getCapability('persistence').persist(); + } + + return this.$q.all( + Object.keys(this.cache) + .map(keyToObject) + .map(objectToPromise)) + .then(function () { + self.transaction = false; + }); + }; + + /** + * Cancel the current transaction, replacing any dirty objects from + * persistence. Not a true rollback, as it cannot be used to undo any + * persist calls that were successful in the event one of a batch of + * persists failing. + * + * @returns {*} + */ + TransactionService.prototype.cancel = function () { + var self = this, + cache = this.cache.get(); + + function keyToObject(key) { + return cache[key]; + } + + function objectToPromise(object) { + return object.getCapability('persistence').refresh(); + } + + return this.$q.all(Object.keys(cache) + .map(keyToObject) + .map(objectToPromise)) + .then(function () { + self.transaction = false; + }); + }; + + return TransactionService; +});