diff --git a/platform/commonUI/browse/src/creation/CreateAction.js b/platform/commonUI/browse/src/creation/CreateAction.js index 83d88ba709..dd8f227446 100644 --- a/platform/commonUI/browse/src/creation/CreateAction.js +++ b/platform/commonUI/browse/src/creation/CreateAction.js @@ -103,7 +103,7 @@ define( if (countEditableViews(editableObject) > 0 && editableObject.hasCapability('composition')) { this.navigationService.setNavigation(editableObject); } else { - return editableObject.getCapability('action').perform('save'); + return editableObject.getCapability('action').perform('save-as'); } }; diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 695ca2aab3..7c8f45eb81 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -32,6 +32,7 @@ define([ "./src/actions/PropertiesAction", "./src/actions/RemoveAction", "./src/actions/SaveAction", + "./src/actions/SaveAsAction", "./src/actions/CancelAction", "./src/policies/EditActionPolicy", "./src/policies/EditableLinkPolicy", @@ -56,6 +57,7 @@ define([ PropertiesAction, RemoveAction, SaveAction, + SaveAsAction, CancelAction, EditActionPolicy, EditableLinkPolicy, @@ -165,6 +167,15 @@ define([ "implementation": SaveAction, "name": "Save", "description": "Save changes made to these objects.", + "depends": [], + "priority": "mandatory" + }, + { + "key": "save-as", + "category": "conclude-editing", + "implementation": SaveAsAction, + "name": "Save As", + "description": "Save changes made to these objects.", "depends": [ "$injector", "policyService", diff --git a/platform/commonUI/edit/src/actions/SaveAction.js b/platform/commonUI/edit/src/actions/SaveAction.js index 1415a6ed4b..ff2a0997fb 100644 --- a/platform/commonUI/edit/src/actions/SaveAction.js +++ b/platform/commonUI/edit/src/actions/SaveAction.js @@ -24,8 +24,8 @@ define( - ['../../../browse/src/creation/CreateWizard'], - function (CreateWizard) { + [], + function () { 'use strict'; /** @@ -37,31 +37,11 @@ define( * @memberof platform/commonUI/edit */ function SaveAction( - $injector, - policyService, - dialogService, - creationService, - copyService, context ) { this.domainObject = (context || {}).domainObject; - this.injectObjectService = function(){ - this.objectService = $injector.get("objectService"); - }; - this.policyService = policyService; - this.dialogService = dialogService; - this.creationService = creationService; - this.copyService = copyService; } - SaveAction.prototype.getObjectService = function(){ - // Lazily acquire object service (avoids cyclical dependency) - if (!this.objectService) { - this.injectObjectService(); - } - return this.objectService; - }; - /** * Save changes and conclude editing. * @@ -70,9 +50,7 @@ define( * @memberof platform/commonUI/edit.SaveAction# */ SaveAction.prototype.perform = function () { - var domainObject = this.domainObject, - copyService = this.copyService, - self = this; + var domainObject = this.domainObject; function resolveWith(object){ return function () { @@ -80,64 +58,13 @@ define( }; } - function doWizardSave(parent) { - var context = domainObject.getCapability("context"), - wizard = new CreateWizard( - domainObject, - parent, - self.policyService - ); - - return self.dialogService - .getUserInput( - wizard.getFormStructure(true), - wizard.getInitialFormValue() - ) - .then(wizard.populateObjectFromInput.bind(wizard)); - } - - function fetchObject(objectId){ - return self.getObjectService().getObjects([objectId]).then(function(objects){ - return objects[objectId]; - }); - } - - function getParent(object){ - return fetchObject(object.getModel().location); - } - - function allowClone(objectToClone) { - return (objectToClone.getId() === domainObject.getId()) || - objectToClone.getCapability('location').isOriginal(); - } - - 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; // this is introduced by EditableDomainObject which is // used to insulate underlying objects from changes made // during editing. function doSave() { - //This is a new 'virtual object' that has not been persisted - // yet. - if (domainObject.getModel().persisted === undefined){ - return getParent(domainObject) - .then(doWizardSave) - .then(getParent) - .then(cloneIntoParent) - .then(cancelEditingAfterClone) - .catch(resolveWith(false)); - } else { - return domainObject.getCapability("editor").save() - .then(resolveWith(domainObject.getOriginalObject())); - } + return domainObject.getCapability("editor").save() + .then(resolveWith(domainObject.getOriginalObject())); } // Discard the current root view (which will be the editing @@ -162,7 +89,8 @@ define( SaveAction.appliesTo = function (context) { var domainObject = (context || {}).domainObject; return domainObject !== undefined && - domainObject.hasCapability("editor"); + domainObject.hasCapability("editor") && + domainObject.getModel().persisted !== undefined; }; return SaveAction; diff --git a/platform/commonUI/edit/src/actions/SaveAsAction.js b/platform/commonUI/edit/src/actions/SaveAsAction.js new file mode 100644 index 0000000000..bfd723c6d5 --- /dev/null +++ b/platform/commonUI/edit/src/actions/SaveAsAction.js @@ -0,0 +1,169 @@ +/***************************************************************************** + * 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*/ +/*jslint es5: true */ + + +define( + ['../../../browse/src/creation/CreateWizard'], + function (CreateWizard) { + 'use strict'; + + /** + * The "Save" action; the action triggered by clicking Save from + * Edit Mode. Exits the editing user interface and invokes object + * capabilities to persist the changes that have been made. + * @constructor + * @implements {Action} + * @memberof platform/commonUI/edit + */ + function SaveAsAction( + $injector, + policyService, + dialogService, + creationService, + copyService, + context + ) { + this.domainObject = (context || {}).domainObject; + this.injectObjectService = function(){ + this.objectService = $injector.get("objectService"); + }; + this.policyService = policyService; + this.dialogService = dialogService; + this.creationService = creationService; + this.copyService = copyService; + } + + /** + * @private + */ + SaveAsAction.prototype.createWizard = function (parent) { + return new CreateWizard( + this.domainObject, + parent, + this.policyService + ); + }; + + /** + * @private + */ + SaveAsAction.prototype.getObjectService = function(){ + // Lazily acquire object service (avoids cyclical dependency) + if (!this.objectService) { + this.injectObjectService(); + } + return this.objectService; + }; + + function resolveWith(object){ + return function () { + return object; + }; + } + + /** + * Save changes and conclude editing. + * + * @returns {Promise} a promise that will be fulfilled when + * cancellation has completed + * @memberof platform/commonUI/edit.SaveAction# + */ + SaveAsAction.prototype.perform = function () { + // Discard the current root view (which will be the editing + // UI, which will have been pushed atop the Browse UI.) + function returnToBrowse(object) { + if (object) { + object.getCapability("action").perform("navigate"); + } + return object; + } + + return this.save().then(returnToBrowse); + }; + + /** + * @private + */ + SaveAsAction.prototype.save = function () { + var self = this, + domainObject = this.domainObject, + copyService = this.copyService; + + function doWizardSave(parent) { + var wizard = self.createWizard(parent); + + return self.dialogService + .getUserInput(wizard.getFormStructure(true), + wizard.getInitialFormValue() + ).then(wizard.populateObjectFromInput.bind(wizard)); + } + + function fetchObject(objectId){ + return self.getObjectService().getObjects([objectId]).then(function(objects){ + return objects[objectId]; + }); + } + + function getParent(object){ + return fetchObject(object.getModel().location); + } + + function allowClone(objectToClone) { + return (objectToClone.getId() === domainObject.getId()) || + objectToClone.getCapability('location').isOriginal(); + } + + function cloneIntoParent(parent) { + return copyService.perform(domainObject, parent, allowClone); + } + + function cancelEditingAfterClone(clonedObject) { + return domainObject.getCapability("editor").cancel() + .then(resolveWith(clonedObject)); + } + + return getParent(domainObject) + .then(doWizardSave) + .then(getParent) + .then(cloneIntoParent) + .then(cancelEditingAfterClone) + .catch(resolveWith(false)); + }; + + /** + * Check if this action is applicable in a given context. + * This will ensure that a domain object is present in the context, + * and that this domain object is in Edit mode. + * @returns true if applicable + */ + SaveAsAction.appliesTo = function (context) { + var domainObject = (context || {}).domainObject; + return domainObject !== undefined && + domainObject.hasCapability("editor") && + domainObject.getModel().persisted === undefined; + }; + + return SaveAsAction; + } +); diff --git a/platform/commonUI/edit/test/actions/SaveActionSpec.js b/platform/commonUI/edit/test/actions/SaveActionSpec.js index 656d6e1ebc..98279e39b1 100644 --- a/platform/commonUI/edit/test/actions/SaveActionSpec.js +++ b/platform/commonUI/edit/test/actions/SaveActionSpec.js @@ -27,11 +27,11 @@ define( "use strict"; describe("The Save action", function () { - var mockLocation, - mockDomainObject, + var mockDomainObject, mockEditorCapability, - mockUrlService, actionContext, + mockActionCapability, + capabilities = {}, action; function mockPromise(value) { @@ -43,65 +43,62 @@ define( } beforeEach(function () { - mockLocation = jasmine.createSpyObj( - "$location", - [ "path" ] - ); mockDomainObject = jasmine.createSpyObj( "domainObject", - [ "getCapability", "hasCapability" ] + [ + "getCapability", + "hasCapability", + "getModel", + "getOriginalObject" + ] ); mockEditorCapability = jasmine.createSpyObj( "editor", [ "save", "cancel" ] ); - mockUrlService = jasmine.createSpyObj( - "urlService", - ["urlForLocation"] + mockActionCapability = jasmine.createSpyObj( + "actionCapability", + [ "perform"] ); - + capabilities.editor = mockEditorCapability; + capabilities.action = mockActionCapability; actionContext = { domainObject: mockDomainObject }; mockDomainObject.hasCapability.andReturn(true); - mockDomainObject.getCapability.andReturn(mockEditorCapability); + mockDomainObject.getCapability.andCallFake(function (capability) { + return capabilities[capability]; + }); + mockDomainObject.getModel.andReturn({persisted: 0}); mockEditorCapability.save.andReturn(mockPromise(true)); + mockDomainObject.getOriginalObject.andReturn(mockDomainObject); - action = new SaveAction(mockLocation, mockUrlService, actionContext); + action = new SaveAction(actionContext); }); it("only applies to domain object with an editor capability", function () { - expect(SaveAction.appliesTo(actionContext)).toBeTruthy(); + expect(SaveAction.appliesTo(actionContext)).toBe(true); expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor"); mockDomainObject.hasCapability.andReturn(false); mockDomainObject.getCapability.andReturn(undefined); - expect(SaveAction.appliesTo(actionContext)).toBeFalsy(); + expect(SaveAction.appliesTo(actionContext)).toBe(false); }); - //TODO: Disabled for NEM Beta - xit("invokes the editor capability's save functionality when performed", function () { - // Verify precondition - expect(mockEditorCapability.save).not.toHaveBeenCalled(); - action.perform(); - - // Should have called cancel - expect(mockEditorCapability.save).toHaveBeenCalled(); - - // Also shouldn't call cancel - expect(mockEditorCapability.cancel).not.toHaveBeenCalled(); + it("only applies to domain object that has already been persisted", + function () { + mockDomainObject.getModel.andReturn({persisted: undefined}); + expect(SaveAction.appliesTo(actionContext)).toBe(false); }); - //TODO: Disabled for NEM Beta - xit("returns to browse when performed", function () { - action.perform(); - expect(mockLocation.path).toHaveBeenCalledWith( - mockUrlService.urlForLocation("browse", mockDomainObject) - ); - }); + it("uses the editor capability to save the object", + function () { + action.perform(); + expect(mockEditorCapability.save).toHaveBeenCalled(); + }); }); } ); \ No newline at end of file diff --git a/platform/commonUI/edit/test/actions/SaveAsActionSpec.js b/platform/commonUI/edit/test/actions/SaveAsActionSpec.js new file mode 100644 index 0000000000..866447a874 --- /dev/null +++ b/platform/commonUI/edit/test/actions/SaveAsActionSpec.js @@ -0,0 +1,174 @@ +/***************************************************************************** + * 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,xit,xdescribe*/ + +define( + ["../../src/actions/SaveAsAction"], + function (SaveAsAction) { + "use strict"; + + describe("The Save As action", function () { + var mockDomainObject, + mockEditorCapability, + mockActionCapability, + mockObjectService, + mockDialogService, + mockCopyService, + mockParent, + mockUrlService, + actionContext, + capabilities = {}, + action; + + function noop () {} + + function mockPromise(value) { + return (value || {}).then ? value : + { + then: function (callback) { + return mockPromise(callback(value)); + }, + catch: function (callback) { + return mockPromise(callback(value)); + } + } ; + } + + beforeEach(function () { + mockDomainObject = jasmine.createSpyObj( + "domainObject", + [ + "getCapability", + "hasCapability", + "getModel" + ] + ); + mockDomainObject.hasCapability.andReturn(true); + mockDomainObject.getCapability.andCallFake(function (capability) { + return capabilities[capability]; + }); + mockDomainObject.getModel.andReturn({location: 'a', persisted: undefined}); + + mockParent = jasmine.createSpyObj( + "parentObject", + [ + "getCapability", + "hasCapability", + "getModel" + ] + ); + + mockEditorCapability = jasmine.createSpyObj( + "editor", + [ "save", "cancel" ] + ); + mockEditorCapability.cancel.andReturn(mockPromise(undefined)); + mockEditorCapability.save.andReturn(mockPromise(true)); + capabilities.editor = mockEditorCapability; + + mockActionCapability = jasmine.createSpyObj( + "action", + ["perform"] + ); + capabilities.action = mockActionCapability; + + mockObjectService = jasmine.createSpyObj( + "objectService", + ["getObjects"] + ); + mockObjectService.getObjects.andReturn(mockPromise({'a': mockParent})); + + mockDialogService = jasmine.createSpyObj( + "dialogService", + [ + "getUserInput" + ] + ); + mockDialogService.getUserInput.andReturn(mockPromise(undefined)); + + mockCopyService = jasmine.createSpyObj( + "copyService", + [ + "perform" + ] + ); + + mockUrlService = jasmine.createSpyObj( + "urlService", + ["urlForLocation"] + ); + + actionContext = { + domainObject: mockDomainObject + }; + + action = new SaveAsAction(undefined, undefined, mockDialogService, undefined, mockCopyService, actionContext); + + spyOn(action, "getObjectService"); + action.getObjectService.andReturn(mockObjectService); + + spyOn(action, "createWizard"); + action.createWizard.andReturn({ + getFormStructure: noop, + getInitialFormValue: noop, + populateObjectFromInput: function() { + return mockDomainObject; + } + }); + + }); + + it("only applies to domain object with an editor capability", function () { + expect(SaveAsAction.appliesTo(actionContext)).toBe(true); + expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor"); + + mockDomainObject.hasCapability.andReturn(false); + mockDomainObject.getCapability.andReturn(undefined); + expect(SaveAsAction.appliesTo(actionContext)).toBe(false); + }); + + it("only applies to domain object that has not already been" + + " persisted", function () { + expect(SaveAsAction.appliesTo(actionContext)).toBe(true); + expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor"); + + mockDomainObject.getModel.andReturn({persisted: 0}); + expect(SaveAsAction.appliesTo(actionContext)).toBe(false); + }); + + it("returns to browse after save", function () { + spyOn(action, "save"); + action.save.andReturn(mockPromise(mockDomainObject)); + action.perform(); + expect(mockActionCapability.perform).toHaveBeenCalledWith( + "navigate" + ); + }); + + it("prompts the user for object details", function () { + action.perform(); + expect(mockDialogService.getUserInput).toHaveBeenCalled(); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/controls/_buttons.scss b/platform/commonUI/general/res/sass/controls/_buttons.scss index ddcca833ba..7b585f8eec 100644 --- a/platform/commonUI/general/res/sass/controls/_buttons.scss +++ b/platform/commonUI/general/res/sass/controls/_buttons.scss @@ -76,6 +76,11 @@ $pad: $interiorMargin * $baseRatio; font-family: symbolsfont; margin-right: $interiorMarginSm; } + &.t-save-as:before { + content:'\e612'; + font-family: symbolsfont; + margin-right: $interiorMarginSm; + } &.t-cancel { .title-label { display: none; } &:before {