[Remove] Add confirmation dialog (#1870)

* [Remove] Added confirmation dialog before the remove action is performed

Addresses #563
This commit is contained in:
tobiasbrown
2018-07-28 06:54:41 +10:00
committed by Andrew Henry
parent 15a75ac134
commit a1d206bfc3
4 changed files with 341 additions and 209 deletions

View File

@ -188,6 +188,7 @@ define([
"name": "Remove", "name": "Remove",
"description": "Remove this object from its containing object.", "description": "Remove this object from its containing object.",
"depends": [ "depends": [
"dialogService",
"navigationService" "navigationService"
] ]
}, },

View File

@ -23,111 +23,119 @@
/** /**
* Module defining RemoveAction. Created by vwoeltje on 11/17/14. * Module defining RemoveAction. Created by vwoeltje on 11/17/14.
*/ */
define( define([
[], './RemoveDialog'
function () { ], function (
RemoveDialog
) {
/** /**
* Construct an action which will remove the provided object manifestation. * Construct an action which will remove the provided object manifestation.
* The object will be removed from its parent's composition; the parent * The object will be removed from its parent's composition; the parent
* is looked up via the "context" capability (so this will be the * is looked up via the "context" capability (so this will be the
* immediate ancestor by which this specific object was reached.) * immediate ancestor by which this specific object was reached.)
* *
* @param {DomainObject} object the object to be removed * @param {DialogService} dialogService a service which will show the dialog
* @param {ActionContext} context the context in which this action is performed * @param {NavigationService} navigationService a service that maintains the current navigation state
* @memberof platform/commonUI/edit * @param {ActionContext} context the context in which this action is performed
* @constructor * @memberof platform/commonUI/edit
* @implements {Action} * @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) { function isNotObject(otherObjectId) {
this.domainObject = (context || {}).domainObject; return otherObjectId !== domainObject.getId();
this.navigationService = navigationService;
} }
/** /*
* Perform this action. * Mutate a parent object such that it no longer contains the object
* @return {Promise} a promise which will be * which is being removed.
* fulfilled when the action has completed.
*/ */
RemoveAction.prototype.perform = function () { function doMutate(model) {
var navigationService = this.navigationService, model.composition = model.composition.filter(isNotObject);
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();
}
/* /*
* Mutate a parent object such that it no longer contains the object * Checks current object and ascendants of current
* which is being removed. * object with object being removed, if the current
*/ * object or any in the current object's path is being removed,
function doMutate(model) { * navigate back to parent of removed object.
model.composition = model.composition.filter(isNotObject); */
} function checkObjectNavigation(object, parentObject) {
// Traverse object starts at current location
var traverseObject = (navigationService).getNavigation(),
context;
/* // Stop when object is not defined (above ROOT)
* Checks current object and ascendants of current while (traverseObject) {
* object with object being removed, if the current // If object currently traversed to is object being removed
* object or any in the current object's path is being removed, // navigate to parent of current object and then exit loop
* navigate back to parent of removed object. if (traverseObject.getId() === object.getId()) {
*/ navigationService.setNavigation(parentObject);
function checkObjectNavigation(object, parentObject) { return;
// 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();
} }
// 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 * Remove the object from its parent, as identified by its context
* capability. Based on object's location and selected object's location * capability. Based on object's location and selected object's location
* user may be navigated to existing parent object * user may be navigated to existing parent object
*/ */
function removeFromContext(object) { function removeFromContext(object) {
var contextCapability = object.getCapability('context'), var contextCapability = object.getCapability('context'),
parent = contextCapability.getParent(); parent = contextCapability.getParent();
// If currently within path of removed object(s), // If currently within path of removed object(s),
// navigates to existing object up tree // navigates to existing object up tree
checkObjectNavigation(object, parent); 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 // Object needs to have a parent for Remove to be applicable
RemoveAction.appliesTo = function (context) { RemoveAction.appliesTo = function (context) {
var object = (context || {}).domainObject, var object = (context || {}).domainObject,
contextCapability = object && object.getCapability("context"), contextCapability = object && object.getCapability("context"),
parent = contextCapability && contextCapability.getParent(), parent = contextCapability && contextCapability.getParent(),
parentType = parent && parent.getCapability('type'), parentType = parent && parent.getCapability('type'),
parentCreatable = parentType && parentType.hasFeature('creation'); parentCreatable = parentType && parentType.hasFeature('creation');
// Only creatable types should be modifiable // Only creatable types should be modifiable
return parent !== undefined && return parent !== undefined &&
Array.isArray(parent.getModel().composition) && Array.isArray(parent.getModel().composition) &&
parentCreatable; parentCreatable;
}; };
return RemoveAction; return RemoveAction;
} });
);

View File

@ -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;
});

View File

@ -25,50 +25,37 @@ define(
function (RemoveAction) { function (RemoveAction) {
describe("The Remove action", function () { describe("The Remove action", function () {
var mockQ, var action,
mockNavigationService,
mockDomainObject,
mockParent,
mockChildObject,
mockGrandchildObject,
mockRootObject,
mockContext,
mockChildContext,
mockGrandchildContext,
mockRootContext,
mockMutation,
mockType,
actionContext, actionContext,
model,
capabilities, capabilities,
action; mockContext,
mockDialogService,
function mockPromise(value) { mockDomainObject,
return { mockMutation,
then: function (callback) { mockNavigationService,
return mockPromise(callback(value)); mockParent,
} mockType,
}; model;
}
beforeEach(function () { beforeEach(function () {
mockDomainObject = jasmine.createSpyObj( mockDomainObject = jasmine.createSpyObj(
"domainObject", "domainObject",
["getId", "getCapability"] ["getId", "getCapability", "getModel"]
); );
mockChildObject = jasmine.createSpyObj(
"domainObject", mockMutation = jasmine.createSpyObj("mutation", ["invoke"]);
["getId", "getCapability"] mockType = jasmine.createSpyObj("type", ["hasFeature"]);
); mockType.hasFeature.and.returnValue(true);
mockGrandchildObject = jasmine.createSpyObj(
"domainObject", capabilities = {
["getId", "getCapability"] mutation: mockMutation,
); type: mockType
mockRootObject = jasmine.createSpyObj( };
"domainObject",
["getId", "getCapability"] model = {
); composition: ["a", "test", "b"]
mockQ = { when: mockPromise }; };
mockParent = { mockParent = {
getModel: function () { getModel: function () {
return model; return model;
@ -80,12 +67,12 @@ define(
return capabilities[k].invoke(v); return capabilities[k].invoke(v);
} }
}; };
mockContext = jasmine.createSpyObj("context", ["getParent"]);
mockChildContext = jasmine.createSpyObj("context", ["getParent"]); mockDialogService = jasmine.createSpyObj(
mockGrandchildContext = jasmine.createSpyObj("context", ["getParent"]); "dialogService",
mockRootContext = jasmine.createSpyObj("context", ["getParent"]); ["showBlockingMessage"]
mockMutation = jasmine.createSpyObj("mutation", ["invoke"]); );
mockType = jasmine.createSpyObj("type", ["hasFeature"]);
mockNavigationService = jasmine.createSpyObj( mockNavigationService = jasmine.createSpyObj(
"navigationService", "navigationService",
[ [
@ -97,23 +84,19 @@ define(
); );
mockNavigationService.getNavigation.and.returnValue(mockDomainObject); mockNavigationService.getNavigation.and.returnValue(mockDomainObject);
mockContext = jasmine.createSpyObj("context", ["getParent"]);
mockContext.getParent.and.returnValue(mockParent);
mockDomainObject.getId.and.returnValue("test"); mockDomainObject.getId.and.returnValue("test");
mockDomainObject.getCapability.and.returnValue(mockContext); mockDomainObject.getCapability.and.returnValue(mockContext);
mockDomainObject.getModel.and.returnValue({name: 'test object'});
mockContext.getParent.and.returnValue(mockParent); mockContext.getParent.and.returnValue(mockParent);
mockType.hasFeature.and.returnValue(true); mockType.hasFeature.and.returnValue(true);
capabilities = {
mutation: mockMutation,
type: mockType
};
model = {
composition: ["a", "test", "b"]
};
actionContext = { domainObject: mockDomainObject }; actionContext = { domainObject: mockDomainObject };
action = new RemoveAction(mockNavigationService, actionContext); action = new RemoveAction(mockDialogService, mockNavigationService, actionContext);
}); });
it("only applies to objects with parents", function () { it("only applies to objects with parents", function () {
@ -127,83 +110,146 @@ define(
expect(mockType.hasFeature).toHaveBeenCalledWith('creation'); expect(mockType.hasFeature).toHaveBeenCalledWith('creation');
}); });
it("mutates the parent when performed", function () { it("shows a blocking message dialog", function () {
action.perform(); mockParent = jasmine.createSpyObj(
expect(mockMutation.invoke) "parent",
.toHaveBeenCalledWith(jasmine.any(Function)); ["getModel", "getCapability", "useCapability"]
}); );
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);
action.perform(); action.perform();
// Expects navigation to parent of domainObject (removed object) expect(mockDialogService.showBlockingMessage).toHaveBeenCalled();
expect(mockNavigationService.setNavigation).toHaveBeenCalledWith(mockParent);
// 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 () { describe("after the remove callback is triggered", function () {
// Navigates to grandchild of ROOT var mockChildContext,
mockNavigationService.getNavigation.and.returnValue(mockGrandchildObject); mockChildObject,
mockDialogHandle,
mockGrandchildContext,
mockGrandchildObject,
mockRootContext,
mockRootObject;
// domainObject (grandparent) is set as ROOT, child and grandchild beforeEach(function () {
// are set objects not being removed mockChildObject = jasmine.createSpyObj(
mockDomainObject.getId.and.returnValue("test 1"); "domainObject",
mockRootObject.getId.and.returnValue("ROOT"); ["getId", "getCapability"]
mockChildObject.getId.and.returnValue("not test 2"); );
mockGrandchildObject.getId.and.returnValue("not test 3");
// Sets context for the grandchild, child, and domainObject mockDialogHandle = jasmine.createSpyObj(
mockRootObject.getCapability.and.returnValue(mockRootContext); "dialogHandle",
mockChildObject.getCapability.and.returnValue(mockChildContext); ["dismiss"]
mockGrandchildObject.getCapability.and.returnValue(mockGrandchildContext); );
// Parents of grandchild and child are set mockGrandchildObject = jasmine.createSpyObj(
mockChildContext.getParent.and.returnValue(mockRootObject); "domainObject",
mockGrandchildContext.getParent.and.returnValue(mockChildObject); ["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();
}); });
}); });
} }
); );