mirror of
https://github.com/nasa/openmct.git
synced 2025-06-18 15:18:12 +00:00
[Edit Mode] Canceling edit mode with unsaved changes now shows confirmation dialog to user. Fixes #664
This commit is contained in:
@ -93,11 +93,9 @@ define([
|
|||||||
"$scope",
|
"$scope",
|
||||||
"$route",
|
"$route",
|
||||||
"$location",
|
"$location",
|
||||||
"$window",
|
|
||||||
"objectService",
|
"objectService",
|
||||||
"navigationService",
|
"navigationService",
|
||||||
"urlService",
|
"urlService",
|
||||||
"policyService",
|
|
||||||
"DEFAULT_PATH"
|
"DEFAULT_PATH"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -201,7 +199,9 @@ define([
|
|||||||
"implementation": NavigateAction,
|
"implementation": NavigateAction,
|
||||||
"depends": [
|
"depends": [
|
||||||
"navigationService",
|
"navigationService",
|
||||||
"$q"
|
"$q",
|
||||||
|
"policyService",
|
||||||
|
"$window"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -44,11 +44,9 @@ define(
|
|||||||
$scope,
|
$scope,
|
||||||
$route,
|
$route,
|
||||||
$location,
|
$location,
|
||||||
$window,
|
|
||||||
objectService,
|
objectService,
|
||||||
navigationService,
|
navigationService,
|
||||||
urlService,
|
urlService,
|
||||||
policyService,
|
|
||||||
defaultPath
|
defaultPath
|
||||||
) {
|
) {
|
||||||
var path = [ROOT_ID].concat(
|
var path = [ROOT_ID].concat(
|
||||||
@ -75,25 +73,10 @@ define(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback for updating the in-scope reference to the object
|
function setScopeObjects(domainObject, navigationAllowed) {
|
||||||
// that is currently navigated-to.
|
|
||||||
function setNavigation(domainObject) {
|
|
||||||
var navigationAllowed = true;
|
|
||||||
|
|
||||||
if (domainObject === $scope.navigatedObject) {
|
|
||||||
//do nothing;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
policyService.allow("navigation", $scope.navigatedObject, domainObject, function (message) {
|
|
||||||
navigationAllowed = $window.confirm(message + "\r\n\r\n" +
|
|
||||||
" Are you sure you want to continue?");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (navigationAllowed) {
|
if (navigationAllowed) {
|
||||||
$scope.navigatedObject = domainObject;
|
$scope.navigatedObject = domainObject;
|
||||||
$scope.treeModel.selectedObject = domainObject;
|
$scope.treeModel.selectedObject = domainObject;
|
||||||
navigationService.setNavigation(domainObject);
|
|
||||||
updateRoute(domainObject);
|
updateRoute(domainObject);
|
||||||
} else {
|
} else {
|
||||||
//If navigation was unsuccessful (ie. blocked), reset
|
//If navigation was unsuccessful (ie. blocked), reset
|
||||||
@ -103,6 +86,20 @@ define(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Callback for updating the in-scope reference to the object
|
||||||
|
// that is currently navigated-to.
|
||||||
|
function setNavigation(domainObject) {
|
||||||
|
if (domainObject === $scope.navigatedObject) {
|
||||||
|
//do nothing;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (domainObject) {
|
||||||
|
domainObject.getCapability("action").perform("navigate").then(setScopeObjects.bind(undefined, domainObject));
|
||||||
|
} else {
|
||||||
|
setScopeObjects(domainObject, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function navigateTo(domainObject) {
|
function navigateTo(domainObject) {
|
||||||
|
|
||||||
// Check if an object has been navigated-to already...
|
// Check if an object has been navigated-to already...
|
||||||
|
@ -33,10 +33,12 @@ define(
|
|||||||
* @constructor
|
* @constructor
|
||||||
* @implements {Action}
|
* @implements {Action}
|
||||||
*/
|
*/
|
||||||
function NavigateAction(navigationService, $q, context) {
|
function NavigateAction(navigationService, $q, policyService, $window, context) {
|
||||||
this.domainObject = context.domainObject;
|
this.domainObject = context.domainObject;
|
||||||
this.$q = $q;
|
this.$q = $q;
|
||||||
this.navigationService = navigationService;
|
this.navigationService = navigationService;
|
||||||
|
this.policyService = policyService;
|
||||||
|
this.$window = $window;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,9 +47,20 @@ define(
|
|||||||
* navigation has been updated
|
* navigation has been updated
|
||||||
*/
|
*/
|
||||||
NavigateAction.prototype.perform = function () {
|
NavigateAction.prototype.perform = function () {
|
||||||
|
var self = this,
|
||||||
|
navigationAllowed = true;
|
||||||
|
|
||||||
|
function allow() {
|
||||||
|
self.policyService.allow("navigation", self.navigationService.getNavigation(), self.domainObject, function (message) {
|
||||||
|
navigationAllowed = self.$window.confirm(message + "\r\n\r\n" +
|
||||||
|
" Are you sure you want to continue?");
|
||||||
|
});
|
||||||
|
return navigationAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
// Set navigation, and wrap like a promise
|
// Set navigation, and wrap like a promise
|
||||||
return this.$q.when(
|
return this.$q.when(
|
||||||
this.navigationService.setNavigation(this.domainObject)
|
allow() && this.navigationService.setNavigation(this.domainObject)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,9 +37,8 @@ define(
|
|||||||
mockUrlService,
|
mockUrlService,
|
||||||
mockDomainObject,
|
mockDomainObject,
|
||||||
mockNextObject,
|
mockNextObject,
|
||||||
mockWindow,
|
|
||||||
mockPolicyService,
|
|
||||||
testDefaultRoot,
|
testDefaultRoot,
|
||||||
|
mockActionCapability,
|
||||||
controller;
|
controller;
|
||||||
|
|
||||||
function mockPromise(value) {
|
function mockPromise(value) {
|
||||||
@ -55,25 +54,14 @@ define(
|
|||||||
mockScope,
|
mockScope,
|
||||||
mockRoute,
|
mockRoute,
|
||||||
mockLocation,
|
mockLocation,
|
||||||
mockWindow,
|
|
||||||
mockObjectService,
|
mockObjectService,
|
||||||
mockNavigationService,
|
mockNavigationService,
|
||||||
mockUrlService,
|
mockUrlService,
|
||||||
mockPolicyService,
|
|
||||||
testDefaultRoot
|
testDefaultRoot
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockWindow = jasmine.createSpyObj('$window', [
|
|
||||||
"confirm"
|
|
||||||
]);
|
|
||||||
mockWindow.confirm.andReturn(true);
|
|
||||||
|
|
||||||
mockPolicyService = jasmine.createSpyObj('policyService', [
|
|
||||||
'allow'
|
|
||||||
]);
|
|
||||||
|
|
||||||
testDefaultRoot = "some-root-level-domain-object";
|
testDefaultRoot = "some-root-level-domain-object";
|
||||||
|
|
||||||
mockScope = jasmine.createSpyObj(
|
mockScope = jasmine.createSpyObj(
|
||||||
@ -128,6 +116,8 @@ define(
|
|||||||
mockNextObject.getId.andReturn("next");
|
mockNextObject.getId.andReturn("next");
|
||||||
mockDomainObject.getId.andReturn(testDefaultRoot);
|
mockDomainObject.getId.andReturn(testDefaultRoot);
|
||||||
|
|
||||||
|
mockActionCapability = jasmine.createSpyObj('actionCapability', ['perform']);
|
||||||
|
|
||||||
instantiateController();
|
instantiateController();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -211,8 +201,13 @@ define(
|
|||||||
mockContext.getPath.andReturn(
|
mockContext.getPath.andReturn(
|
||||||
[mockRootObject, mockDomainObject, mockNextObject]
|
[mockRootObject, mockDomainObject, mockNextObject]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Return true from navigate action
|
||||||
|
mockActionCapability.perform.andReturn(mockPromise(true));
|
||||||
|
|
||||||
mockNextObject.getCapability.andCallFake(function (c) {
|
mockNextObject.getCapability.andCallFake(function (c) {
|
||||||
return c === 'context' && mockContext;
|
return (c === 'context' && mockContext) ||
|
||||||
|
(c === 'action' && mockActionCapability);
|
||||||
});
|
});
|
||||||
mockScope.$on.andReturn(mockUnlisten);
|
mockScope.$on.andReturn(mockUnlisten);
|
||||||
// Provide a navigation change
|
// Provide a navigation change
|
||||||
@ -225,6 +220,7 @@ define(
|
|||||||
mockLocation.path.andReturn("/browse/");
|
mockLocation.path.andReturn("/browse/");
|
||||||
|
|
||||||
mockNavigationService.setNavigation.andReturn(true);
|
mockNavigationService.setNavigation.andReturn(true);
|
||||||
|
mockActionCapability.perform.andReturn(mockPromise(true));
|
||||||
|
|
||||||
// Exercise the Angular workaround
|
// Exercise the Angular workaround
|
||||||
mockNavigationService.addListener.mostRecentCall.args[0]();
|
mockNavigationService.addListener.mostRecentCall.args[0]();
|
||||||
@ -243,6 +239,9 @@ define(
|
|||||||
mockScope.navigatedObject = mockDomainObject;
|
mockScope.navigatedObject = mockDomainObject;
|
||||||
mockNavigationService.setNavigation.andReturn(true);
|
mockNavigationService.setNavigation.andReturn(true);
|
||||||
|
|
||||||
|
mockActionCapability.perform.andReturn(mockPromise(true));
|
||||||
|
mockNextObject.getCapability.andReturn(mockActionCapability);
|
||||||
|
|
||||||
//Simulate a change in selected tree object
|
//Simulate a change in selected tree object
|
||||||
mockScope.treeModel = {selectedObject: mockDomainObject};
|
mockScope.treeModel = {selectedObject: mockDomainObject};
|
||||||
mockScope.$watch.mostRecentCall.args[1](mockNextObject);
|
mockScope.$watch.mostRecentCall.args[1](mockNextObject);
|
||||||
@ -254,11 +253,10 @@ define(
|
|||||||
it("after failed navigation event resets the selected tree" +
|
it("after failed navigation event resets the selected tree" +
|
||||||
" object", function () {
|
" object", function () {
|
||||||
mockScope.navigatedObject = mockDomainObject;
|
mockScope.navigatedObject = mockDomainObject;
|
||||||
mockWindow.confirm.andReturn(false);
|
|
||||||
mockPolicyService.allow.andCallFake(function (category, object, context, callback) {
|
//Return false from navigation action
|
||||||
callback("unsaved changes");
|
mockActionCapability.perform.andReturn(mockPromise(false));
|
||||||
return false;
|
mockNextObject.getCapability.andReturn(mockActionCapability);
|
||||||
});
|
|
||||||
|
|
||||||
//Simulate a change in selected tree object
|
//Simulate a change in selected tree object
|
||||||
mockScope.treeModel = {selectedObject: mockDomainObject};
|
mockScope.treeModel = {selectedObject: mockDomainObject};
|
||||||
|
@ -31,6 +31,8 @@ define(
|
|||||||
var mockNavigationService,
|
var mockNavigationService,
|
||||||
mockQ,
|
mockQ,
|
||||||
mockDomainObject,
|
mockDomainObject,
|
||||||
|
mockPolicyService,
|
||||||
|
mockWindow,
|
||||||
action;
|
action;
|
||||||
|
|
||||||
function mockPromise(value) {
|
function mockPromise(value) {
|
||||||
@ -44,25 +46,70 @@ define(
|
|||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockNavigationService = jasmine.createSpyObj(
|
mockNavigationService = jasmine.createSpyObj(
|
||||||
"navigationService",
|
"navigationService",
|
||||||
["setNavigation"]
|
[
|
||||||
|
"setNavigation",
|
||||||
|
"getNavigation"
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
mockNavigationService.getNavigation.andReturn({});
|
||||||
mockQ = { when: mockPromise };
|
mockQ = { when: mockPromise };
|
||||||
mockDomainObject = jasmine.createSpyObj(
|
mockDomainObject = jasmine.createSpyObj(
|
||||||
"domainObject",
|
"domainObject",
|
||||||
["getId", "getModel", "getCapability"]
|
["getId", "getModel", "getCapability"]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
mockPolicyService = jasmine.createSpyObj("policyService",
|
||||||
|
[
|
||||||
|
"allow"
|
||||||
|
]);
|
||||||
|
mockWindow = jasmine.createSpyObj("$window",
|
||||||
|
[
|
||||||
|
"confirm"
|
||||||
|
]);
|
||||||
|
|
||||||
action = new NavigateAction(
|
action = new NavigateAction(
|
||||||
mockNavigationService,
|
mockNavigationService,
|
||||||
mockQ,
|
mockQ,
|
||||||
|
mockPolicyService,
|
||||||
|
mockWindow,
|
||||||
{ domainObject: mockDomainObject }
|
{ domainObject: mockDomainObject }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("invokes the navigate service when performed", function () {
|
it("invokes the policy service to determine if navigation" +
|
||||||
|
" allowed", function () {
|
||||||
|
action.perform();
|
||||||
|
expect(mockPolicyService.allow)
|
||||||
|
.toHaveBeenCalledWith("navigation", jasmine.any(Object), jasmine.any(Object), jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prompts user if policy rejection", function () {
|
||||||
|
action.perform();
|
||||||
|
expect(mockPolicyService.allow).toHaveBeenCalled();
|
||||||
|
mockPolicyService.allow.mostRecentCall.args[3]();
|
||||||
|
expect(mockWindow.confirm).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shows a prompt", function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
// Ensure the allow callback is called synchronously
|
||||||
|
mockPolicyService.allow.andCallFake(function () {
|
||||||
|
return arguments[3]();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("does not navigate on prompt rejection", function () {
|
||||||
|
mockWindow.confirm.andReturn(false);
|
||||||
action.perform();
|
action.perform();
|
||||||
expect(mockNavigationService.setNavigation)
|
expect(mockNavigationService.setNavigation)
|
||||||
.toHaveBeenCalledWith(mockDomainObject);
|
.not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does navigate on prompt acceptance", function () {
|
||||||
|
mockWindow.confirm.andReturn(true);
|
||||||
|
action.perform();
|
||||||
|
expect(mockNavigationService.setNavigation)
|
||||||
|
.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is only applicable when a domain object is in context", function () {
|
it("is only applicable when a domain object is in context", function () {
|
||||||
|
@ -50,18 +50,24 @@ define(
|
|||||||
//If the object existed already, navigate to refresh view
|
//If the object existed already, navigate to refresh view
|
||||||
// with previous object state.
|
// with previous object state.
|
||||||
if (domainObject.getModel().persisted) {
|
if (domainObject.getModel().persisted) {
|
||||||
domainObject.getCapability("action").perform("navigate");
|
return domainObject.getCapability("action").perform("navigate");
|
||||||
} else {
|
} else {
|
||||||
//If the object was new, and user has cancelled, then
|
//If the object was new, and user has cancelled, then
|
||||||
//navigate back to parent because nothing to show.
|
//navigate back to parent because nothing to show.
|
||||||
domainObject.getCapability("location").getOriginal().then(function (original) {
|
return domainObject.getCapability("location").getOriginal().then(function (original) {
|
||||||
parent = original.getCapability("context").getParent();
|
parent = original.getCapability("context").getParent();
|
||||||
parent.getCapability("action").perform("navigate");
|
parent.getCapability("action").perform("navigate");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.domainObject.getCapability("editor").cancel()
|
|
||||||
.then(returnToBrowse);
|
function cancel(allowed) {
|
||||||
|
return allowed && domainObject.getCapability("editor").cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Do navigation first in order to trigger unsaved changes dialog
|
||||||
|
return returnToBrowse()
|
||||||
|
.then(cancel);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,6 +125,9 @@ define(
|
|||||||
|
|
||||||
it("invokes the editor capability's cancel functionality when" +
|
it("invokes the editor capability's cancel functionality when" +
|
||||||
" performed", function () {
|
" performed", function () {
|
||||||
|
mockDomainObject.getModel.andReturn({persisted: 1});
|
||||||
|
//Return true from navigate action
|
||||||
|
capabilities.action.perform.andReturn(mockPromise(true));
|
||||||
action.perform();
|
action.perform();
|
||||||
|
|
||||||
// Should have called cancel
|
// Should have called cancel
|
||||||
@ -134,13 +137,15 @@ define(
|
|||||||
expect(capabilities.editor.save).not.toHaveBeenCalled();
|
expect(capabilities.editor.save).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("navigates to object if existing", function () {
|
it("navigates to object if existing using navigate action", function () {
|
||||||
mockDomainObject.getModel.andReturn({persisted: 1});
|
mockDomainObject.getModel.andReturn({persisted: 1});
|
||||||
|
//Return true from navigate action
|
||||||
|
capabilities.action.perform.andReturn(mockPromise(true));
|
||||||
action.perform();
|
action.perform();
|
||||||
expect(capabilities.action.perform).toHaveBeenCalledWith("navigate");
|
expect(capabilities.action.perform).toHaveBeenCalledWith("navigate");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("navigates to parent if new", function () {
|
it("navigates to parent if new using navigate action", function () {
|
||||||
mockDomainObject.getModel.andReturn({persisted: undefined});
|
mockDomainObject.getModel.andReturn({persisted: undefined});
|
||||||
action.perform();
|
action.perform();
|
||||||
expect(parentCapabilities.action.perform).toHaveBeenCalledWith("navigate");
|
expect(parentCapabilities.action.perform).toHaveBeenCalledWith("navigate");
|
||||||
|
@ -72,10 +72,10 @@ define(
|
|||||||
* Maintain a configuration object on scope that stores column
|
* Maintain a configuration object on scope that stores column
|
||||||
* configuration. On change, synchronize with object model.
|
* configuration. On change, synchronize with object model.
|
||||||
*/
|
*/
|
||||||
$scope.$watchCollection('configuration.table.columns', function (columns) {
|
$scope.$watchCollection('configuration.table.columns', function (newColumns, oldColumns) {
|
||||||
if (columns) {
|
if (newColumns !== oldColumns) {
|
||||||
self.domainObject.useCapability('mutation', function (model) {
|
self.domainObject.useCapability('mutation', function (model) {
|
||||||
model.configuration.table.columns = columns;
|
model.configuration.table.columns = newColumns;
|
||||||
});
|
});
|
||||||
self.domainObject.getCapability('persistence').persist();
|
self.domainObject.getCapability('persistence').persist();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user