diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 8e20c283c0..63910dbdf6 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -188,6 +188,7 @@ define([ "name": "Remove", "description": "Remove this object from its containing object.", "depends": [ + "dialogService", "navigationService" ] }, diff --git a/platform/commonUI/edit/src/actions/RemoveAction.js b/platform/commonUI/edit/src/actions/RemoveAction.js index ee03c8e62c..3c921b316b 100644 --- a/platform/commonUI/edit/src/actions/RemoveAction.js +++ b/platform/commonUI/edit/src/actions/RemoveAction.js @@ -23,111 +23,119 @@ /** * Module defining RemoveAction. Created by vwoeltje on 11/17/14. */ -define( - [], - function () { +define([ + './RemoveDialog' +], function ( + RemoveDialog +) { - /** - * Construct an action which will remove the provided object manifestation. - * The object will be removed from its parent's composition; the parent - * is looked up via the "context" capability (so this will be the - * immediate ancestor by which this specific object was reached.) - * - * @param {DomainObject} object the object to be removed - * @param {ActionContext} context the context in which this action is performed - * @memberof platform/commonUI/edit - * @constructor - * @implements {Action} + /** + * Construct an action which will remove the provided object manifestation. + * The object will be removed from its parent's composition; the parent + * is looked up via the "context" capability (so this will be the + * immediate ancestor by which this specific object was reached.) + * + * @param {DialogService} dialogService a service which will show the dialog + * @param {NavigationService} navigationService a service that maintains the current navigation state + * @param {ActionContext} context the context in which this action is performed + * @memberof platform/commonUI/edit + * @constructor + * @implements {Action} + */ + function RemoveAction(dialogService, navigationService, context) { + this.domainObject = (context || {}).domainObject; + this.dialogService = dialogService; + this.navigationService = navigationService; + } + + /** + * Perform this action. + */ + RemoveAction.prototype.perform = function () { + var dialog, + dialogService = this.dialogService, + domainObject = this.domainObject, + navigationService = this.navigationService; + /* + * Check whether an object ID matches the ID of the object being + * removed (used to filter a parent's composition to handle the + * removal.) */ - function RemoveAction(navigationService, context) { - this.domainObject = (context || {}).domainObject; - this.navigationService = navigationService; + function isNotObject(otherObjectId) { + return otherObjectId !== domainObject.getId(); } - /** - * Perform this action. - * @return {Promise} a promise which will be - * fulfilled when the action has completed. + /* + * Mutate a parent object such that it no longer contains the object + * which is being removed. */ - RemoveAction.prototype.perform = function () { - var navigationService = this.navigationService, - domainObject = this.domainObject; - /* - * Check whether an object ID matches the ID of the object being - * removed (used to filter a parent's composition to handle the - * removal.) - */ - function isNotObject(otherObjectId) { - return otherObjectId !== domainObject.getId(); - } + function doMutate(model) { + model.composition = model.composition.filter(isNotObject); + } - /* - * Mutate a parent object such that it no longer contains the object - * which is being removed. - */ - function doMutate(model) { - model.composition = model.composition.filter(isNotObject); - } + /* + * Checks current object and ascendants of current + * object with object being removed, if the current + * object or any in the current object's path is being removed, + * navigate back to parent of removed object. + */ + function checkObjectNavigation(object, parentObject) { + // Traverse object starts at current location + var traverseObject = (navigationService).getNavigation(), + context; - /* - * Checks current object and ascendants of current - * object with object being removed, if the current - * object or any in the current object's path is being removed, - * navigate back to parent of removed object. - */ - function checkObjectNavigation(object, parentObject) { - // Traverse object starts at current location - var traverseObject = (navigationService).getNavigation(), - context; - - // Stop when object is not defined (above ROOT) - while (traverseObject) { - // If object currently traversed to is object being removed - // navigate to parent of current object and then exit loop - if (traverseObject.getId() === object.getId()) { - navigationService.setNavigation(parentObject); - return; - } - // Traverses to parent of current object, moving - // up the ascendant path - context = traverseObject.getCapability('context'); - traverseObject = context && context.getParent(); + // Stop when object is not defined (above ROOT) + while (traverseObject) { + // If object currently traversed to is object being removed + // navigate to parent of current object and then exit loop + if (traverseObject.getId() === object.getId()) { + navigationService.setNavigation(parentObject); + return; } + // Traverses to parent of current object, moving + // up the ascendant path + context = traverseObject.getCapability('context'); + traverseObject = context && context.getParent(); } + } - /* - * Remove the object from its parent, as identified by its context - * capability. Based on object's location and selected object's location - * user may be navigated to existing parent object - */ - function removeFromContext(object) { - var contextCapability = object.getCapability('context'), - parent = contextCapability.getParent(); + /* + * Remove the object from its parent, as identified by its context + * capability. Based on object's location and selected object's location + * user may be navigated to existing parent object + */ + function removeFromContext(object) { + var contextCapability = object.getCapability('context'), + parent = contextCapability.getParent(); - // If currently within path of removed object(s), - // navigates to existing object up tree - checkObjectNavigation(object, parent); + // If currently within path of removed object(s), + // navigates to existing object up tree + checkObjectNavigation(object, parent); - return parent.useCapability('mutation', doMutate); - } + return parent.useCapability('mutation', doMutate); + } - return removeFromContext(domainObject); - }; + /* + * Pass in the function to remove the domain object so it can be + * associated with an 'OK' button press + */ + dialog = new RemoveDialog(dialogService, domainObject, removeFromContext); + dialog.show(); + }; - // Object needs to have a parent for Remove to be applicable - RemoveAction.appliesTo = function (context) { - var object = (context || {}).domainObject, - contextCapability = object && object.getCapability("context"), - parent = contextCapability && contextCapability.getParent(), - parentType = parent && parent.getCapability('type'), - parentCreatable = parentType && parentType.hasFeature('creation'); + // Object needs to have a parent for Remove to be applicable + RemoveAction.appliesTo = function (context) { + var object = (context || {}).domainObject, + contextCapability = object && object.getCapability("context"), + parent = contextCapability && contextCapability.getParent(), + parentType = parent && parent.getCapability('type'), + parentCreatable = parentType && parentType.hasFeature('creation'); - // Only creatable types should be modifiable - return parent !== undefined && - Array.isArray(parent.getModel().composition) && - parentCreatable; - }; + // Only creatable types should be modifiable + return parent !== undefined && + Array.isArray(parent.getModel().composition) && + parentCreatable; + }; - return RemoveAction; - } -); + return RemoveAction; +}); diff --git a/platform/commonUI/edit/src/actions/RemoveDialog.js b/platform/commonUI/edit/src/actions/RemoveDialog.js new file mode 100644 index 0000000000..049b26ba0f --- /dev/null +++ b/platform/commonUI/edit/src/actions/RemoveDialog.js @@ -0,0 +1,77 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define([], function () { + + /** + * @callback removeCallback + * @param {DomainObject} domainObject the domain object to be removed + */ + + /** + * Construct a new Remove dialog. + * + * @param {DialogService} dialogService the service that shows the dialog + * @param {DomainObject} domainObject the domain object to be removed + * @param {removeCallback} removeCallback callback that handles removal of the domain object + * @memberof platform/commonUI/edit + * @constructor + */ + function RemoveDialog(dialogService, domainObject, removeCallback) { + this.dialogService = dialogService; + this.domainObject = domainObject; + this.removeCallback = removeCallback; + } + + /** + * Display a dialog to confirm the removal of a domain object. + */ + RemoveDialog.prototype.show = function () { + var dialog, + domainObject = this.domainObject, + removeCallback = this.removeCallback, + model = { + title: 'Remove ' + domainObject.getModel().name, + actionText: 'Warning! This action will permanently remove this object. Are you sure you want to continue?', + severity: 'alert', + primaryOption: { + label: 'OK', + callback: function () { + removeCallback(domainObject); + dialog.dismiss(); + } + }, + options: [ + { + label: 'Cancel', + callback: function () { + dialog.dismiss(); + } + } + ] + }; + + dialog = this.dialogService.showBlockingMessage(model); + }; + + return RemoveDialog; +}); diff --git a/platform/commonUI/edit/test/actions/RemoveActionSpec.js b/platform/commonUI/edit/test/actions/RemoveActionSpec.js index f9b5089d74..fdf686df4e 100644 --- a/platform/commonUI/edit/test/actions/RemoveActionSpec.js +++ b/platform/commonUI/edit/test/actions/RemoveActionSpec.js @@ -25,50 +25,37 @@ define( function (RemoveAction) { describe("The Remove action", function () { - var mockQ, - mockNavigationService, - mockDomainObject, - mockParent, - mockChildObject, - mockGrandchildObject, - mockRootObject, - mockContext, - mockChildContext, - mockGrandchildContext, - mockRootContext, - mockMutation, - mockType, + var action, actionContext, - model, capabilities, - action; - - function mockPromise(value) { - return { - then: function (callback) { - return mockPromise(callback(value)); - } - }; - } + mockContext, + mockDialogService, + mockDomainObject, + mockMutation, + mockNavigationService, + mockParent, + mockType, + model; beforeEach(function () { mockDomainObject = jasmine.createSpyObj( "domainObject", - ["getId", "getCapability"] + ["getId", "getCapability", "getModel"] ); - mockChildObject = jasmine.createSpyObj( - "domainObject", - ["getId", "getCapability"] - ); - mockGrandchildObject = jasmine.createSpyObj( - "domainObject", - ["getId", "getCapability"] - ); - mockRootObject = jasmine.createSpyObj( - "domainObject", - ["getId", "getCapability"] - ); - mockQ = { when: mockPromise }; + + mockMutation = jasmine.createSpyObj("mutation", ["invoke"]); + mockType = jasmine.createSpyObj("type", ["hasFeature"]); + mockType.hasFeature.and.returnValue(true); + + capabilities = { + mutation: mockMutation, + type: mockType + }; + + model = { + composition: ["a", "test", "b"] + }; + mockParent = { getModel: function () { return model; @@ -80,12 +67,12 @@ define( return capabilities[k].invoke(v); } }; - mockContext = jasmine.createSpyObj("context", ["getParent"]); - mockChildContext = jasmine.createSpyObj("context", ["getParent"]); - mockGrandchildContext = jasmine.createSpyObj("context", ["getParent"]); - mockRootContext = jasmine.createSpyObj("context", ["getParent"]); - mockMutation = jasmine.createSpyObj("mutation", ["invoke"]); - mockType = jasmine.createSpyObj("type", ["hasFeature"]); + + mockDialogService = jasmine.createSpyObj( + "dialogService", + ["showBlockingMessage"] + ); + mockNavigationService = jasmine.createSpyObj( "navigationService", [ @@ -97,23 +84,19 @@ define( ); mockNavigationService.getNavigation.and.returnValue(mockDomainObject); + mockContext = jasmine.createSpyObj("context", ["getParent"]); + mockContext.getParent.and.returnValue(mockParent); mockDomainObject.getId.and.returnValue("test"); mockDomainObject.getCapability.and.returnValue(mockContext); + mockDomainObject.getModel.and.returnValue({name: 'test object'}); + mockContext.getParent.and.returnValue(mockParent); mockType.hasFeature.and.returnValue(true); - capabilities = { - mutation: mockMutation, - type: mockType - }; - model = { - composition: ["a", "test", "b"] - }; - actionContext = { domainObject: mockDomainObject }; - action = new RemoveAction(mockNavigationService, actionContext); + action = new RemoveAction(mockDialogService, mockNavigationService, actionContext); }); it("only applies to objects with parents", function () { @@ -127,83 +110,146 @@ define( expect(mockType.hasFeature).toHaveBeenCalledWith('creation'); }); - it("mutates the parent when performed", function () { - action.perform(); - expect(mockMutation.invoke) - .toHaveBeenCalledWith(jasmine.any(Function)); - }); - - it("changes composition from its mutation function", function () { - var mutator, result; - action.perform(); - mutator = mockMutation.invoke.calls.mostRecent().args[0]; - result = mutator(model); - - // Should not have cancelled the mutation - expect(result).not.toBe(false); - - // Simulate mutate's behavior (remove can either return a - // new model or modify this one in-place) - result = result || model; - - // Should have removed "test" - that was our - // mock domain object's id. - expect(result.composition).toEqual(["a", "b"]); - }); - - it("removes parent of object currently navigated to", function () { - // Navigates to child object - mockNavigationService.getNavigation.and.returnValue(mockChildObject); - - // Test is id of object being removed - // Child object has different id - mockDomainObject.getId.and.returnValue("test"); - mockChildObject.getId.and.returnValue("not test"); - - // Sets context for the child and domainObject - mockDomainObject.getCapability.and.returnValue(mockContext); - mockChildObject.getCapability.and.returnValue(mockChildContext); - - // Parents of child and domainObject are set - mockContext.getParent.and.returnValue(mockParent); - mockChildContext.getParent.and.returnValue(mockDomainObject); - - mockType.hasFeature.and.returnValue(true); + it("shows a blocking message dialog", function () { + mockParent = jasmine.createSpyObj( + "parent", + ["getModel", "getCapability", "useCapability"] + ); action.perform(); - // Expects navigation to parent of domainObject (removed object) - expect(mockNavigationService.setNavigation).toHaveBeenCalledWith(mockParent); + expect(mockDialogService.showBlockingMessage).toHaveBeenCalled(); + + // Also check that no mutation happens at this point + expect(mockParent.useCapability).not.toHaveBeenCalledWith("mutation", jasmine.any(Function)); }); - it("checks if removing object not in ascendent path (reaches ROOT)", function () { - // Navigates to grandchild of ROOT - mockNavigationService.getNavigation.and.returnValue(mockGrandchildObject); + describe("after the remove callback is triggered", function () { + var mockChildContext, + mockChildObject, + mockDialogHandle, + mockGrandchildContext, + mockGrandchildObject, + mockRootContext, + mockRootObject; - // domainObject (grandparent) is set as ROOT, child and grandchild - // are set objects not being removed - mockDomainObject.getId.and.returnValue("test 1"); - mockRootObject.getId.and.returnValue("ROOT"); - mockChildObject.getId.and.returnValue("not test 2"); - mockGrandchildObject.getId.and.returnValue("not test 3"); + beforeEach(function () { + mockChildObject = jasmine.createSpyObj( + "domainObject", + ["getId", "getCapability"] + ); - // Sets context for the grandchild, child, and domainObject - mockRootObject.getCapability.and.returnValue(mockRootContext); - mockChildObject.getCapability.and.returnValue(mockChildContext); - mockGrandchildObject.getCapability.and.returnValue(mockGrandchildContext); + mockDialogHandle = jasmine.createSpyObj( + "dialogHandle", + ["dismiss"] + ); - // Parents of grandchild and child are set - mockChildContext.getParent.and.returnValue(mockRootObject); - mockGrandchildContext.getParent.and.returnValue(mockChildObject); + mockGrandchildObject = jasmine.createSpyObj( + "domainObject", + ["getId", "getCapability"] + ); - mockType.hasFeature.and.returnValue(true); + mockRootObject = jasmine.createSpyObj( + "domainObject", + ["getId", "getCapability"] + ); - action.perform(); + mockChildContext = jasmine.createSpyObj("context", ["getParent"]); + mockGrandchildContext = jasmine.createSpyObj("context", ["getParent"]); + mockRootContext = jasmine.createSpyObj("context", ["getParent"]); + + mockDialogService.showBlockingMessage.and.returnValue(mockDialogHandle); + }); + + it("mutates the parent when performed", function () { + action.perform(); + mockDialogService.showBlockingMessage.calls.mostRecent().args[0] + .primaryOption.callback(); + + expect(mockMutation.invoke) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it("changes composition from its mutation function", function () { + var mutator, result; + + action.perform(); + mockDialogService.showBlockingMessage.calls.mostRecent().args[0] + .primaryOption.callback(); + + mutator = mockMutation.invoke.calls.mostRecent().args[0]; + result = mutator(model); + + // Should not have cancelled the mutation + expect(result).not.toBe(false); + + // Simulate mutate's behavior (remove can either return a + // new model or modify this one in-place) + result = result || model; + + // Should have removed "test" - that was our + // mock domain object's id. + expect(result.composition).toEqual(["a", "b"]); + }); + + it("removes parent of object currently navigated to", function () { + // Navigates to child object + mockNavigationService.getNavigation.and.returnValue(mockChildObject); + + // Test is id of object being removed + // Child object has different id + mockDomainObject.getId.and.returnValue("test"); + mockChildObject.getId.and.returnValue("not test"); + + // Sets context for the child and domainObject + mockDomainObject.getCapability.and.returnValue(mockContext); + mockChildObject.getCapability.and.returnValue(mockChildContext); + + // Parents of child and domainObject are set + mockContext.getParent.and.returnValue(mockParent); + mockChildContext.getParent.and.returnValue(mockDomainObject); + + mockType.hasFeature.and.returnValue(true); + + action.perform(); + mockDialogService.showBlockingMessage.calls.mostRecent().args[0] + .primaryOption.callback(); + + // Expects navigation to parent of domainObject (removed object) + expect(mockNavigationService.setNavigation).toHaveBeenCalledWith(mockParent); + }); + + it("checks if removing object not in ascendent path (reaches ROOT)", function () { + // Navigates to grandchild of ROOT + mockNavigationService.getNavigation.and.returnValue(mockGrandchildObject); + + // domainObject (grandparent) is set as ROOT, child and grandchild + // are set objects not being removed + mockDomainObject.getId.and.returnValue("test 1"); + mockRootObject.getId.and.returnValue("ROOT"); + mockChildObject.getId.and.returnValue("not test 2"); + mockGrandchildObject.getId.and.returnValue("not test 3"); + + // Sets context for the grandchild, child, and domainObject + mockRootObject.getCapability.and.returnValue(mockRootContext); + mockChildObject.getCapability.and.returnValue(mockChildContext); + mockGrandchildObject.getCapability.and.returnValue(mockGrandchildContext); + + // Parents of grandchild and child are set + mockChildContext.getParent.and.returnValue(mockRootObject); + mockGrandchildContext.getParent.and.returnValue(mockChildObject); + + mockType.hasFeature.and.returnValue(true); + + action.perform(); + mockDialogService.showBlockingMessage.calls.mostRecent().args[0] + .primaryOption.callback(); + + // Expects no navigation to occur + expect(mockNavigationService.setNavigation).not.toHaveBeenCalled(); + }); - // Expects no navigation to occur - expect(mockNavigationService.setNavigation).not.toHaveBeenCalled(); }); - }); } );