Merge pull request #1366 from nasa/fix-navigation-warnings-1360

Fix navigation warnings, tidy editing, navigation, browse logic.
This commit is contained in:
Victor Woeltjen 2016-12-21 12:38:54 -08:00 committed by GitHub
commit c4d47ddc26
29 changed files with 405 additions and 903 deletions

View File

@ -1338,20 +1338,6 @@ are supported:
Open MCT defines several Angular directives that are intended for use both
internally within the platform, and by plugins.
## Before Unload
The `mct-before-unload` directive is used to listen for (and prompt for user
confirmation) of navigation changes in the browser. This includes reloading,
following links out of Open MCT, or changing routes. It is used to hook into
both `onbeforeunload` event handling as well as route changes from within
Angular.
This directive is useable as an attribute. Its value should be an Angular
expression. When an action that would trigger an unload and/or route change
occurs, this Angular expression is evaluated. Its result should be a message to
display to the user to confirm their navigation change; if this expression
evaluates to a falsy value, no message will be displayed.
## Chart

View File

@ -41,7 +41,6 @@ define([
"text!./res/templates/items/items.html",
"text!./res/templates/browse/object-properties.html",
"text!./res/templates/browse/inspector-region.html",
"text!./res/templates/view-object.html",
'legacyRegistry'
], function (
BrowseController,
@ -64,7 +63,6 @@ define([
itemsTemplate,
objectPropertiesTemplate,
inspectorRegionTemplate,
viewObjectTemplate,
legacyRegistry
) {
@ -141,10 +139,6 @@ define([
}
],
"representations": [
{
"key": "view-object",
"template": viewObjectTemplate
},
{
"key": "browse-object",
"template": browseObjectTemplate,
@ -204,7 +198,10 @@ define([
"services": [
{
"key": "navigationService",
"implementation": NavigationService
"implementation": NavigationService,
"depends": [
"$window"
]
}
],
"actions": [
@ -212,10 +209,7 @@ define([
"key": "navigate",
"implementation": NavigateAction,
"depends": [
"navigationService",
"$q",
"policyService",
"$window"
"navigationService"
]
},
{

View File

@ -63,7 +63,7 @@
<mct-split-pane class='l-object-and-inspector contents abs' anchor='right'>
<div class='split-pane-component t-object pane primary-pane left'>
<mct-representation mct-object="navigatedObject"
key="'view-object'"
key="navigatedObject.getCapability('status').get('editing') ? 'edit-object' : 'browse-object'"
class="abs holder holder-object">
</mct-representation>
<a class="mini-tab-icon anchor-right mobile-hide toggle-pane toggle-inspect flush-right"

View File

@ -1,33 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2016, 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.
-->
<!--
A representation that allows the 'View' region of an object view to change
dynamically (eg. between browse and edit modes). Values correspond to a
representation key, and currently defaults to 'browse-object'.
In the case of edit, the EditRepresenter will change this to editable
representation of the object as needed.
-->
<mct-representation mct-object="domainObject"
key="viewObjectTemplate || 'browse-object'"
class="abs holder">
</mct-representation>

View File

@ -48,11 +48,16 @@ define(
defaultPath
) {
var initialPath = ($route.current.params.ids || defaultPath).split("/");
var currentIds = $route.current.params.ids;
var currentIds;
$scope.treeModel = {
selectedObject: undefined
selectedObject: undefined,
onSelection: function (object) {
navigationService.setNavigation(object, true);
},
allowSelection: function (object) {
return navigationService.shouldNavigate();
}
};
function idsForObject(domainObject) {
@ -103,7 +108,6 @@ define(
function navigateToObject(desiredObject) {
$scope.navigatedObject = desiredObject;
$scope.treeModel.selectedObject = desiredObject;
navigationService.setNavigation(desiredObject);
currentIds = idsForObject(desiredObject);
$route.current.pathParams.ids = currentIds;
$location.path('/browse/' + currentIds);
@ -114,10 +118,11 @@ define(
.then(function (root) {
return findViaComposition(root, path);
})
.then(navigateToObject);
.then(function (object) {
navigationService.setNavigation(object);
});
}
getObject('ROOT')
.then(function (root) {
$scope.domainObject = root;
@ -137,15 +142,6 @@ define(
// Listen for changes in navigation state.
navigationService.addListener(navigateDirectlyToModel);
// Also listen for changes which come from the tree. Changes in
// the tree will trigger a change in browse navigation state.
$scope.$watch("treeModel.selectedObject", function (newObject, oldObject) {
if (oldObject !== newObject) {
navigateDirectlyToModel(newObject);
}
});
// Listen for route changes which are caused by browser events
// (e.g. bookmarks to pages in OpenMCT) and prevent them. Instead,
// navigate to the path ourselves, which results in it being

View File

@ -33,12 +33,9 @@ define(
* @constructor
* @implements {Action}
*/
function NavigateAction(navigationService, $q, policyService, $window, context) {
function NavigateAction(navigationService, context) {
this.domainObject = context.domainObject;
this.$q = $q;
this.navigationService = navigationService;
this.policyService = policyService;
this.$window = $window;
}
/**
@ -47,36 +44,12 @@ define(
* navigation has been updated
*/
NavigateAction.prototype.perform = function () {
var self = this,
navigateTo = this.domainObject,
currentObject = self.navigationService.getNavigation();
function allow() {
var navigationAllowed = true;
self.policyService.allow("navigation", currentObject, navigateTo, function (message) {
navigationAllowed = self.$window.confirm(message + "\r\n\r\n" +
" Are you sure you want to continue?");
});
return navigationAllowed;
}
function cancelIfEditing() {
var editing = currentObject.hasCapability('editor') &&
currentObject.getCapability('editor').isEditContextRoot();
return self.$q.when(editing && currentObject.getCapability("editor").finish());
}
function navigate() {
return self.navigationService.setNavigation(navigateTo);
}
if (allow()) {
return cancelIfEditing().then(navigate);
} else {
return this.$q.when(false);
if (this.navigationService.shouldNavigate()) {
this.navigationService.setNavigation(this.domainObject, true);
return Promise.resolve({});
}
return Promise.reject('Navigation Prevented by User');
};
/**

View File

@ -30,16 +30,23 @@ define(
/**
* The navigation service maintains the application's current
* navigation state, and allows listening for changes thereto.
*
* @memberof platform/commonUI/browse
* @constructor
*/
function NavigationService() {
function NavigationService($window) {
this.navigated = undefined;
this.callbacks = [];
this.checks = [];
this.$window = $window;
this.oldUnload = $window.onbeforeunload;
$window.onbeforeunload = this.onBeforeUnload.bind(this);
}
/**
* Get the current navigation state.
*
* @returns {DomainObject} the object that is navigated-to
*/
NavigationService.prototype.getNavigation = function () {
@ -47,16 +54,33 @@ define(
};
/**
* Set the current navigation state. This will invoke listeners.
* Navigate to a specified object. If navigation checks exist and
* return reasons to prevent navigation, it will prompt the user before
* continuing. Trying to navigate to the currently navigated object will
* do nothing.
*
* If a truthy value is passed for `force`, it will skip navigation
* and will not prevent navigation to an already selected object.
*
* @param {DomainObject} domainObject the domain object to navigate to
* @param {Boolean} force if true, force navigation to occur.
* @returns {Boolean} true if navigation occured, otherwise false.
*/
NavigationService.prototype.setNavigation = function (value) {
if (this.navigated !== value) {
this.navigated = value;
this.callbacks.forEach(function (callback) {
callback(value);
});
NavigationService.prototype.setNavigation = function (domainObject, force) {
if (force) {
this.doNavigation(domainObject);
return true;
}
if (this.navigated === domainObject) {
return true;
}
var doNotNavigate = this.shouldWarnBeforeNavigate();
if (doNotNavigate && !this.$window.confirm(doNotNavigate)) {
return false;
}
this.doNavigation(domainObject);
return true;
};
@ -64,6 +88,7 @@ define(
* Listen for changes in navigation. The passed callback will
* be invoked with the new domain object of navigation when
* this changes.
*
* @param {function} callback the callback to invoke when
* navigation state changes
*/
@ -73,6 +98,7 @@ define(
/**
* Stop listening for changes in navigation state.
*
* @param {function} callback the callback which should
* no longer be invoked when navigation state
* changes
@ -83,6 +109,89 @@ define(
});
};
/**
* Check if navigation should proceed. May prompt a user for input
* if any checkFns return messages. Returns true if the user wishes to
* navigate, otherwise false. If using this prior to calling
* `setNavigation`, you should call `setNavigation` with `force=true`
* to prevent duplicate dialogs being displayed to the user.
*
* @returns {Boolean} true if the user wishes to navigate, otherwise false.
*/
NavigationService.prototype.shouldNavigate = function () {
var doNotNavigate = this.shouldWarnBeforeNavigate();
return !doNotNavigate || this.$window.confirm(doNotNavigate);
};
/**
* Register a check function to be called before any navigation occurs.
* Check functions should return a human readable "message" if
* there are any reasons to prevent navigation. Otherwise, they should
* return falsy. Returns a function which can be called to remove the
* check function.
*
* @param {Function} checkFn a function to call before navigation occurs.
* @returns {Function} removeCheck call to remove check
*/
NavigationService.prototype.checkBeforeNavigation = function (checkFn) {
this.checks.push(checkFn);
return function removeCheck() {
this.checks = this.checks.filter(function (fn) {
return checkFn !== fn;
});
}.bind(this);
};
/**
* Private method to actually perform navigation.
*
* @private
*/
NavigationService.prototype.doNavigation = function (value) {
this.navigated = value;
this.callbacks.forEach(function (callback) {
callback(value);
});
};
/**
* Returns either a false value, or a string that should be displayed
* to the user before navigation is allowed.
*
* @private
*/
NavigationService.prototype.shouldWarnBeforeNavigate = function () {
var reasons = [];
this.checks.forEach(function (checkFn) {
var reason = checkFn();
if (reason) {
reasons.push(reason);
}
});
if (reasons.length) {
return reasons.join('\n');
}
return false;
};
/**
* Listener for window on before unload event-- will warn before
* navigation is allowed.
*
* @private
*/
NavigationService.prototype.onBeforeUnload = function () {
var shouldWarnBeforeNavigate = this.shouldWarnBeforeNavigate();
if (shouldWarnBeforeNavigate) {
return shouldWarnBeforeNavigate;
}
if (this.oldUnload) {
return this.oldUnload.apply(undefined, [].slice.apply(arguments));
}
};
return NavigationService;
}
);

View File

@ -24,8 +24,14 @@
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../src/BrowseController"],
function (BrowseController) {
[
"../src/BrowseController",
"../src/navigation/NavigationService"
],
function (
BrowseController,
NavigationService
) {
describe("The browse controller", function () {
var mockScope,
@ -44,7 +50,7 @@ define(
function waitsForNavigation() {
var calls = mockNavigationService.setNavigation.calls.length;
waitsFor(function () {
return mockNavigationService.setNavigation.calls.length > calls ;
return mockNavigationService.setNavigation.calls.length > calls;
});
}
@ -92,15 +98,16 @@ define(
"objectService",
["getObjects"]
);
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[
"getNavigation",
"setNavigation",
"addListener",
"removeListener"
]
);
mockNavigationService = new NavigationService({});
[
"getNavigation",
"setNavigation",
"addListener",
"removeListener"
].forEach(function (method) {
spyOn(mockNavigationService, method)
.andCallThrough();
});
mockRootObject = jasmine.createSpyObj(
"rootObjectContainer",
["getId", "getCapability", "getModel", "useCapability", "hasCapability"]

View File

@ -23,145 +23,74 @@
/**
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../../src/navigation/NavigateAction"],
function (NavigateAction) {
define([
"../../src/navigation/NavigateAction"
], function (
NavigateAction
) {
describe("The navigate action", function () {
var mockNavigationService,
mockQ,
mockDomainObject,
mockPolicyService,
mockNavigatedObject,
mockWindow,
capabilities,
action;
describe("The navigate action", function () {
var mockNavigationService,
mockDomainObject,
action;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
capabilities = {};
mockQ = { when: mockPromise };
mockNavigatedObject = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel",
"hasCapability",
"getCapability"
]
);
capabilities.editor = jasmine.createSpyObj("editorCapability", [
"isEditContextRoot",
"finish"
]);
mockNavigatedObject.getCapability.andCallFake(function (capability) {
return capabilities[capability];
});
mockNavigatedObject.hasCapability.andReturn(false);
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[
"setNavigation",
"getNavigation"
]
);
mockNavigationService.getNavigation.andReturn(mockNavigatedObject);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel"
]
);
mockPolicyService = jasmine.createSpyObj("policyService",
[
"allow"
]);
mockWindow = jasmine.createSpyObj("$window",
[
"confirm"
]);
action = new NavigateAction(
mockNavigationService,
mockQ,
mockPolicyService,
mockWindow,
{ domainObject: mockDomainObject }
);
function waitForCall() {
var called = false;
waitsFor(function () {
return called;
});
return function () {
called = true;
};
}
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));
});
beforeEach(function () {
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[
"shouldNavigate",
"setNavigation"
]
);
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();
expect(mockNavigationService.setNavigation)
.not.toHaveBeenCalled();
});
it("does navigate on prompt acceptance", function () {
mockWindow.confirm.andReturn(true);
action.perform();
expect(mockNavigationService.setNavigation)
.toHaveBeenCalled();
});
});
describe("in edit mode", function () {
beforeEach(function () {
mockNavigatedObject.hasCapability.andCallFake(function (capability) {
return capability === "editor";
});
capabilities.editor.isEditContextRoot.andReturn(true);
});
it("finishes editing if in edit mode", function () {
action.perform();
expect(capabilities.editor.finish)
.toHaveBeenCalled();
});
});
it("is only applicable when a domain object is in context", function () {
expect(NavigateAction.appliesTo({})).toBeFalsy();
expect(NavigateAction.appliesTo({
domainObject: mockDomainObject
})).toBeTruthy();
});
mockDomainObject = {};
action = new NavigateAction(
mockNavigationService,
{ domainObject: mockDomainObject }
);
});
}
);
it("sets navigation if it is allowed", function () {
mockNavigationService.shouldNavigate.andReturn(true);
action.perform()
.then(waitForCall());
runs(function () {
expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockDomainObject, true);
});
});
it("does not set navigation if it is not allowed", function () {
mockNavigationService.shouldNavigate.andReturn(false);
var onSuccess = jasmine.createSpy('onSuccess');
action.perform()
.then(onSuccess, waitForCall());
runs(function () {
expect(onSuccess).not.toHaveBeenCalled();
expect(mockNavigationService.setNavigation)
.not
.toHaveBeenCalledWith(mockDomainObject);
});
});
it("is only applicable when a domain object is in context", function () {
expect(NavigateAction.appliesTo({})).toBeFalsy();
expect(NavigateAction.appliesTo({
domainObject: mockDomainObject
})).toBeTruthy();
});
});
});

View File

@ -28,10 +28,12 @@ define(
function (NavigationService) {
describe("The navigation service", function () {
var navigationService;
var $window,
navigationService;
beforeEach(function () {
navigationService = new NavigationService();
$window = jasmine.createSpyObj('$window', ['confirm']);
navigationService = new NavigationService($window);
});
it("stores navigation state", function () {

View File

@ -2,25 +2,6 @@ Contains sources and resources associated with Edit mode.
# Extensions
## Directives
This bundle introduces the `mct-before-unload` directive, primarily for
internal use (to prompt the user to confirm navigation away from unsaved
changes in Edit mode.)
The `mct-before-unload` directive is used as an attribute whose value is
an Angular expression that is evaluated when navigation changes (either
via browser-level changes, such as the refresh button, or changes to
the Angular route, which happens when hitting the back button in Edit
mode.) The result of this evaluation, when truthy, is shown in a browser
dialog to allow the user to confirm navigation. When falsy, no prompt is
shown, allowing these dialogs to be shown conditionally. (For instance, in
Edit mode, prompts are only shown if user-initiated changes have
occurred.)
This directive may be attached to any element; its behavior will be enforced
so long as that element remains within the DOM.
# Toolbars
Views may specify the contents of a toolbar through a `toolbar`

View File

@ -25,7 +25,6 @@ define([
"./src/controllers/EditPanesController",
"./src/controllers/ElementsController",
"./src/controllers/EditObjectController",
"./src/directives/MCTBeforeUnload",
"./src/actions/EditAndComposeAction",
"./src/actions/EditAction",
"./src/actions/PropertiesAction",
@ -37,7 +36,6 @@ define([
"./src/policies/EditActionPolicy",
"./src/policies/EditableLinkPolicy",
"./src/policies/EditableMovePolicy",
"./src/policies/EditNavigationPolicy",
"./src/policies/EditContextualActionPolicy",
"./src/representers/EditRepresenter",
"./src/representers/EditToolbarRepresenter",
@ -65,7 +63,6 @@ define([
EditPanesController,
ElementsController,
EditObjectController,
MCTBeforeUnload,
EditAndComposeAction,
EditAction,
PropertiesAction,
@ -77,7 +74,6 @@ define([
EditActionPolicy,
EditableLinkPolicy,
EditableMovePolicy,
EditNavigationPolicy,
EditContextualActionPolicy,
EditRepresenter,
EditToolbarRepresenter,
@ -132,7 +128,7 @@ define([
"depends": [
"$scope",
"$location",
"policyService"
"navigationService"
]
},
{
@ -152,15 +148,6 @@ define([
]
}
],
"directives": [
{
"key": "mctBeforeUnload",
"implementation": MCTBeforeUnload,
"depends": [
"$window"
]
}
],
"actions": [
{
"key": "compose",
@ -273,11 +260,6 @@ define([
"category": "action",
"implementation": EditableLinkPolicy
},
{
"category": "navigation",
"message": "Continuing will cause the loss of any unsaved changes.",
"implementation": EditNavigationPolicy
},
{
"implementation": CreationPolicy,
"category": "creation"

View File

@ -20,8 +20,7 @@
at runtime from the About dialog for additional information.
-->
<div class="abs l-flex-col" ng-controller="EditObjectController as EditObjectController">
<div mct-before-unload="EditObjectController.getUnloadWarning()"
class="holder flex-elem l-flex-row object-browse-bar ">
<div class="holder flex-elem l-flex-row object-browse-bar ">
<div class="items-select left flex-elem l-flex-row grows">
<mct-representation key="'back-arrow'"
mct-object="domainObject"

View File

@ -56,13 +56,13 @@ define(
//navigate back to parent because nothing to show.
return domainObject.getCapability("location").getOriginal().then(function (original) {
parent = original.getCapability("context").getParent();
parent.getCapability("action").perform("navigate");
return parent.getCapability("action").perform("navigate");
});
}
}
function cancel(allowed) {
return allowed && domainObject.getCapability("editor").finish();
function cancel() {
return domainObject.getCapability("editor").finish();
}
//Do navigation first in order to trigger unsaved changes dialog

View File

@ -28,18 +28,49 @@ define(
[],
function () {
function isDirty(domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor");
return editorCapability &&
editorCapability.isEditContextRoot() &&
editorCapability.dirty();
}
function cancelEditing(domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor");
return editorCapability &&
editorCapability.finish();
}
/**
* Controller which is responsible for populating the scope for
* Edit mode
* @memberof platform/commonUI/edit
* @constructor
*/
function EditObjectController($scope, $location, policyService) {
function EditObjectController($scope, $location, navigationService) {
this.scope = $scope;
this.policyService = policyService;
var domainObject = $scope.domainObject;
var navigatedObject;
function setViewForDomainObject(domainObject) {
var removeCheck = navigationService
.checkBeforeNavigation(function () {
if (isDirty(domainObject)) {
return "Continuing will cause the loss of any unsaved changes.";
}
return false;
});
$scope.$on('$destroy', function () {
removeCheck();
cancelEditing(domainObject);
});
function setViewForDomainObject() {
var locationViewKey = $location.search().view;
@ -54,34 +85,15 @@ define(
((domainObject && domainObject.useCapability('view')) || [])
.forEach(selectViewIfMatching);
}
navigatedObject = domainObject;
}
$scope.$watch('domainObject', setViewForDomainObject);
setViewForDomainObject();
$scope.doAction = function (action) {
return $scope[action] && $scope[action]();
};
}
/**
* Get the warning to show if the user attempts to navigate
* away from Edit mode while unsaved changes are present.
* @returns {string} the warning to show, or undefined if
* there are no unsaved changes
*/
EditObjectController.prototype.getUnloadWarning = function () {
var navigatedObject = this.scope.domainObject,
policyMessage;
this.policyService.allow("navigation", navigatedObject, undefined, function (message) {
policyMessage = message;
});
return policyMessage;
};
return EditObjectController;
}
);

View File

@ -1,104 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, 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 () {
/**
* Defines the `mct-before-unload` directive. The expression bound
* to this attribute will be evaluated during page navigation events
* and, if it returns a truthy value, will be used to populate a
* prompt to the user to confirm this navigation.
* @memberof platform/commonUI/edit
* @constructor
* @param $window the window
*/
function MCTBeforeUnload($window) {
var unloads = [],
oldBeforeUnload = $window.onbeforeunload;
// Run all unload functions, returning the first returns truthily.
function checkUnloads() {
var result;
unloads.forEach(function (unload) {
result = result || unload();
});
return result;
}
// Link function for an mct-before-unload directive usage
function link(scope, element, attrs) {
// Invoke the
function unload() {
return scope.$eval(attrs.mctBeforeUnload);
}
// Stop using this unload expression
function removeUnload() {
unloads = unloads.filter(function (callback) {
return callback !== unload;
});
if (unloads.length === 0) {
$window.onbeforeunload = oldBeforeUnload;
}
}
// Show a dialog before allowing a location change
function checkLocationChange(event) {
// Get an unload message (if any)
var warning = unload();
// Prompt the user if there's an unload message
if (warning && !$window.confirm(warning)) {
// ...and prevent the route change if it was confirmed
event.preventDefault();
}
}
// If this is the first active instance of this directive,
// register as the window's beforeunload handler
if (unloads.length === 0) {
$window.onbeforeunload = checkUnloads;
}
// Include this instance of the directive's unload function
unloads.push(unload);
// Remove it when the scope is destroyed
scope.$on("$destroy", removeUnload);
// Also handle route changes
scope.$on("$locationChangeStart", checkLocationChange);
}
return {
// Applicable as an attribute
restrict: "A",
// Link with the provided function
link: link
};
}
return MCTBeforeUnload;
}
);

View File

@ -1,64 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, 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 () {
/**
* Policy controlling whether navigation events should proceed
* when object is being edited.
* @memberof platform/commonUI/edit
* @constructor
* @implements {Policy.<Action, ActionContext>}
*/
function EditNavigationPolicy(policyService) {
this.policyService = policyService;
}
/**
* @private
*/
EditNavigationPolicy.prototype.isDirty = function (domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor");
return editorCapability &&
editorCapability.isEditContextRoot() &&
editorCapability.dirty();
};
/**
* Allow navigation if an object is not dirty, or if the user elects
* to proceed anyway.
* @param currentNavigation
* @returns {boolean|*} true if the object model is clean; or if
* it's dirty and the user wishes to proceed anyway.
*/
EditNavigationPolicy.prototype.allow = function (currentNavigation) {
return !this.isDirty(currentNavigation);
};
return EditNavigationPolicy;
}
);

View File

@ -47,7 +47,6 @@ define(
var self = this;
this.scope = scope;
this.listenHandle = undefined;
// Mutate and persist a new version of a domain object's model.
function doMutate(model) {
@ -90,51 +89,15 @@ define(
// Place the "commit" method in the scope
scope.commit = commit;
// Clean up when the scope is destroyed
scope.$on("$destroy", function () {
self.destroy();
});
}
// Handle a specific representation of a specific domain object
EditRepresenter.prototype.represent = function represent(representation, representedObject) {
var scope = this.scope;
// Track the key, to know which view configuration to save to.
this.key = (representation || {}).key;
// Track the represented object
EditRepresenter.prototype.represent = function (representation, representedObject) {
this.domainObject = representedObject;
// Ensure existing watches are released
this.destroy();
function setEditing() {
scope.viewObjectTemplate = 'edit-object';
}
/**
* Listen for changes in object state. If the object becomes
* editable then change the view and inspector regions
* object representation accordingly
*/
this.listenHandle = this.domainObject.getCapability('status').listen(function (statuses) {
if (statuses.indexOf('editing') !== -1) {
setEditing();
} else {
delete scope.viewObjectTemplate;
}
});
if (representedObject.hasCapability('editor') && representedObject.getCapability('editor').isEditContextRoot()) {
setEditing();
}
};
// Respond to the destruction of the current representation.
EditRepresenter.prototype.destroy = function destroy() {
return this.listenHandle && this.listenHandle();
};
EditRepresenter.prototype.destroy = function () {};
return EditRepresenter;
}

View File

@ -24,32 +24,19 @@ define(
["../../src/controllers/EditObjectController"],
function (EditObjectController) {
describe("The Edit mode controller", function () {
describe("The Edit Object controller", function () {
var mockScope,
mockObject,
mockType,
testViews,
mockEditorCapability,
mockLocation,
mockNavigationService,
removeCheck,
mockStatusCapability,
mockCapabilities,
mockPolicyService,
controller;
// Utility function; look for a $watch on scope and fire it
function fireWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockPolicyService = jasmine.createSpyObj(
"policyService",
[
"allow"
]
);
mockScope = jasmine.createSpyObj(
"$scope",
["$on", "$watch"]
@ -58,16 +45,16 @@ define(
"domainObject",
["getId", "getModel", "getCapability", "hasCapability", "useCapability"]
);
mockType = jasmine.createSpyObj(
"type",
["hasFeature"]
mockEditorCapability = jasmine.createSpyObj(
"mockEditorCapability",
["isEditContextRoot", "dirty", "finish"]
);
mockStatusCapability = jasmine.createSpyObj('statusCapability',
["get"]
);
mockCapabilities = {
"type" : mockType,
"editor" : mockEditorCapability,
"status": mockStatusCapability
};
@ -75,52 +62,70 @@ define(
["search"]
);
mockLocation.search.andReturn({"view": "fixed"});
mockNavigationService = jasmine.createSpyObj('navigationService',
["checkBeforeNavigation"]
);
removeCheck = jasmine.createSpy('removeCheck');
mockNavigationService.checkBeforeNavigation.andReturn(removeCheck);
mockObject.getId.andReturn("test");
mockObject.getModel.andReturn({ name: "Test object" });
mockObject.getCapability.andCallFake(function (key) {
return mockCapabilities[key];
});
mockType.hasFeature.andReturn(true);
mockScope.domainObject = mockObject;
controller = new EditObjectController(
mockScope,
mockLocation,
mockPolicyService
);
});
it("exposes a warning message for unload", function () {
var errorMessage = "Unsaved changes";
// Normally, should be undefined
expect(controller.getUnloadWarning()).toBeUndefined();
// Override the policy service to prevent navigation
mockPolicyService.allow.andCallFake(function (category, object, context, callback) {
callback(errorMessage);
});
// Should have some warning message here now
expect(controller.getUnloadWarning()).toEqual(errorMessage);
});
it("sets the active view from query parameters", function () {
var testViews = [
{ key: 'abc' },
{ key: 'def', someKey: 'some value' },
{ key: 'xyz' }
];
testViews = [
{ key: 'abc' },
{ key: 'def', someKey: 'some value' },
{ key: 'xyz' }
];
mockObject.useCapability.andCallFake(function (c) {
return (c === 'view') && testViews;
});
mockLocation.search.andReturn({ view: 'def' });
fireWatch('domainObject', mockObject);
mockScope.domainObject = mockObject;
controller = new EditObjectController(
mockScope,
mockLocation,
mockNavigationService
);
});
it("adds a check before navigation", function () {
expect(mockNavigationService.checkBeforeNavigation)
.toHaveBeenCalledWith(jasmine.any(Function));
var checkFn = mockNavigationService.checkBeforeNavigation.mostRecentCall.args[0];
mockEditorCapability.isEditContextRoot.andReturn(false);
mockEditorCapability.dirty.andReturn(false);
expect(checkFn()).toBe(false);
mockEditorCapability.isEditContextRoot.andReturn(true);
expect(checkFn()).toBe(false);
mockEditorCapability.dirty.andReturn(true);
expect(checkFn())
.toBe("Continuing will cause the loss of any unsaved changes.");
});
it("cleans up on destroy", function () {
expect(mockScope.$on)
.toHaveBeenCalledWith("$destroy", jasmine.any(Function));
mockScope.$on.mostRecentCall.args[1]();
expect(mockEditorCapability.finish).toHaveBeenCalled();
expect(removeCheck).toHaveBeenCalled();
});
it("sets the active view from query parameters", function () {
expect(mockScope.representation.selected)
.toEqual(testViews[1]);
});

View File

@ -1,114 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, 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(
["../../src/directives/MCTBeforeUnload"],
function (MCTBeforeUnload) {
describe("The mct-before-unload directive", function () {
var mockWindow,
mockScope,
testAttrs,
mockEvent,
directive;
function fireListener(eventType, value) {
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === eventType) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockWindow = jasmine.createSpyObj("$window", ['confirm']);
mockScope = jasmine.createSpyObj("$scope", ['$eval', '$on']);
testAttrs = { mctBeforeUnload: "someExpression" };
mockEvent = jasmine.createSpyObj("event", ["preventDefault"]);
directive = new MCTBeforeUnload(mockWindow);
directive.link(mockScope, {}, testAttrs);
});
it("can be used only as an attribute", function () {
expect(directive.restrict).toEqual('A');
});
it("listens for beforeunload", function () {
expect(mockWindow.onbeforeunload).toEqual(jasmine.any(Function));
});
it("listens for route changes", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
"$locationChangeStart",
jasmine.any(Function)
);
});
it("listens for its scope's destroy event", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
"$destroy",
jasmine.any(Function)
);
});
it("uses result of evaluated expression as a warning", function () {
mockScope.$eval.andReturn(undefined);
expect(mockWindow.onbeforeunload(mockEvent)).toBeUndefined();
mockScope.$eval.andReturn("some message");
expect(mockWindow.onbeforeunload(mockEvent)).toEqual("some message");
// Verify that the right expression was evaluated
expect(mockScope.$eval).toHaveBeenCalledWith(testAttrs.mctBeforeUnload);
});
it("confirms route changes", function () {
// First, try with no unsaved changes;
// should not confirm or preventDefault
mockScope.$eval.andReturn(undefined);
fireListener("$locationChangeStart", mockEvent);
expect(mockWindow.confirm).not.toHaveBeenCalled();
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
// Next, try with unsaved changes that the user confirms;
// should prompt, but not preventDefault
mockScope.$eval.andReturn("some message");
mockWindow.confirm.andReturn(true);
fireListener("$locationChangeStart", mockEvent);
expect(mockWindow.confirm).toHaveBeenCalledWith("some message");
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
// Finally, act as if the user said no to this dialog;
// this should preventDefault on the location change.
mockWindow.confirm.andReturn(false);
fireListener("$locationChangeStart", mockEvent);
expect(mockWindow.confirm).toHaveBeenCalledWith("some message");
expect(mockEvent.preventDefault).toHaveBeenCalled();
});
it("cleans up listeners when destroyed", function () {
fireListener("$destroy", mockEvent);
expect(mockWindow.onbeforeunload).toBeUndefined();
});
});
}
);

View File

@ -1,122 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, 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(
["../../src/representers/EditRepresenter"],
function (EditRepresenter) {
describe("The Edit mode representer", function () {
var mockQ,
mockLog,
mockScope,
testRepresentation,
mockDomainObject,
mockStatusCapability,
mockEditorCapability,
mockCapabilities,
representer;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
mockQ = { when: mockPromise };
mockLog = jasmine.createSpyObj("$log", ["info", "debug"]);
mockScope = jasmine.createSpyObj("$scope", ["$watch", "$on"]);
testRepresentation = { key: "test" };
mockDomainObject = jasmine.createSpyObj("domainObject", [
"getId",
"getModel",
"getCapability",
"useCapability",
"hasCapability"
]);
mockStatusCapability =
jasmine.createSpyObj("statusCapability", ["listen"]);
mockEditorCapability =
jasmine.createSpyObj("editorCapability", ["isEditContextRoot"]);
mockCapabilities = {
'status': mockStatusCapability,
'editor': mockEditorCapability
};
mockDomainObject.getModel.andReturn({});
mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.useCapability.andReturn(true);
mockDomainObject.getCapability.andCallFake(function (capability) {
return mockCapabilities[capability];
});
representer = new EditRepresenter(mockQ, mockLog, mockScope);
representer.represent(testRepresentation, mockDomainObject);
});
it("provides a commit method in scope", function () {
expect(mockScope.commit).toEqual(jasmine.any(Function));
});
it("Sets edit view template on edit mode", function () {
mockStatusCapability.listen.mostRecentCall.args[0](['editing']);
mockEditorCapability.isEditContextRoot.andReturn(true);
expect(mockScope.viewObjectTemplate).toEqual('edit-object');
});
it("Cleans up listeners on scope destroy", function () {
representer.listenHandle = jasmine.createSpy('listen');
mockScope.$on.mostRecentCall.args[1]();
expect(representer.listenHandle).toHaveBeenCalled();
});
it("mutates upon observed changes", function () {
mockScope.model = { someKey: "some value" };
mockScope.configuration = { someConfiguration: "something" };
mockScope.commit("Some message");
// Should have mutated the object...
expect(mockDomainObject.useCapability).toHaveBeenCalledWith(
"mutation",
jasmine.any(Function)
);
// Finally, check that the provided mutation function
// includes both model and configuration
expect(
mockDomainObject.useCapability.mostRecentCall.args[1]()
).toEqual({
someKey: "some value",
configuration: {
test: { someConfiguration: "something" }
}
});
});
});
}
);

View File

@ -19,6 +19,9 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<mct-tree mct-object="domainObject" mct-model="ngModel.selectedObject">
<mct-tree root-object="domainObject"
selected-object="ngModel.selectedObject"
on-selection="ngModel.onSelection"
allow-selection="ngModel.allowSelection">
</mct-tree>

View File

@ -26,25 +26,54 @@ define([
], function (angular, TreeView) {
function MCTTree(gestureService) {
function link(scope, element) {
var treeView = new TreeView(gestureService),
unobserve = treeView.observe(function (domainObject) {
if (scope.mctModel !== domainObject) {
scope.mctModel = domainObject;
scope.$apply();
}
});
if (!scope.allowSelection) {
scope.allowSelection = function () {
return true;
};
}
if (!scope.onSelection) {
scope.onSelection = function () {};
}
var currentSelection = scope.selectedObject;
var treeView = new TreeView(gestureService);
function setSelection(domainObject, event) {
if (currentSelection === domainObject) {
return;
}
if (!scope.allowSelection(domainObject)) {
treeView.value(currentSelection);
return;
}
currentSelection = domainObject;
scope.onSelection(domainObject);
scope.selectedObject = domainObject;
if (event && event instanceof MouseEvent) {
scope.$apply();
}
}
var unobserve = treeView.observe(setSelection);
element.append(angular.element(treeView.elements()));
scope.$watch('mctModel', treeView.value.bind(treeView));
scope.$watch('mctObject', treeView.model.bind(treeView));
scope.$watch('selectedObject', function (object) {
currentSelection = object;
treeView.value(object);
});
scope.$watch('rootObject', treeView.model.bind(treeView));
scope.$on('$destroy', unobserve);
}
return {
restrict: "E",
link: link,
scope: { mctObject: "=", mctModel: "=" }
scope: {
rootObject: "=",
selectedObject: "=",
onSelection: "=?",
allowSelection: "=?"
}
};
}

View File

@ -49,8 +49,8 @@ define([
this.labelView = new TreeLabelView(gestureService);
$(this.labelView.elements()).on('click', function () {
selectFn(this.activeObject);
$(this.labelView.elements()).on('click', function (event) {
selectFn(this.activeObject, event);
}.bind(this));
this.li.append($(nodeTemplate));

View File

@ -109,11 +109,11 @@ define([
}.bind(this));
};
TreeView.prototype.value = function (domainObject) {
TreeView.prototype.value = function (domainObject, event) {
this.selectedObject = domainObject;
this.updateNodeViewSelection();
this.callbacks.forEach(function (callback) {
callback(domainObject);
callback(domainObject, event);
});
};

View File

@ -19,10 +19,12 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global console*/
define([
'../../src/directives/MCTTree'
], function (MCTTree) {
'../../src/directives/MCTTree',
'../../src/ui/TreeView'
], function (MCTTree, TreeView) {
describe("The mct-tree directive", function () {
var mockParse,
mockGestureService,
@ -50,6 +52,7 @@ define([
mockExpr = jasmine.createSpy('expr');
mockExpr.assign = jasmine.createSpy('assign');
mockParse.andReturn(mockExpr);
spyOn(TreeView.prototype, 'observe').andCallThrough();
mctTree = new MCTTree(mockParse, mockGestureService);
});
@ -58,8 +61,13 @@ define([
expect(mctTree.restrict).toEqual("E");
});
it("two-way binds to mctObject and mctModel", function () {
expect(mctTree.scope).toEqual({ mctObject: "=", mctModel: "=" });
it("two-way binds", function () {
expect(mctTree.scope).toEqual({
rootObject: "=",
selectedObject: "=",
allowSelection: "=?",
onSelection: "=?"
});
});
describe("link", function () {
@ -81,16 +89,16 @@ define([
expect(mockElement.append).toHaveBeenCalled();
});
it("watches for mct-model's expression in the parent", function () {
it("watches for selected-object expression in the parent", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"mctModel",
"selectedObject",
jasmine.any(Function)
);
});
it("watches for changes to mct-object", function () {
it("watches for changes to root-object", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"mctObject",
"rootObject",
jasmine.any(Function)
);
});
@ -102,6 +110,10 @@ define([
);
});
it("watches for changes in tree view", function () {
});
// https://github.com/nasa/openmct/issues/1114
it("does not trigger $apply during $watches", function () {
mockScope.mctObject = makeMockDomainObject('root');
@ -111,14 +123,18 @@ define([
});
expect(mockScope.$apply).not.toHaveBeenCalled();
});
it("does trigger $apply from other value changes", function () {
it("does trigger $apply from tree manipulation", function () {
if (/PhantomJS/g.test(window.navigator.userAgent)) {
console.log('Unable to run test in PhantomJS due to lack of support for event constructors');
return;
}
// White-boxy; we know this is the setter for the tree's value
var treeValueFn = mockScope.$watch.calls[0].args[1];
var treeValueFn = TreeView.prototype.observe.calls[0].args[0];
mockScope.mctObject = makeMockDomainObject('root');
mockScope.mctMode = makeMockDomainObject('selection');
treeValueFn(makeMockDomainObject('other'));
treeValueFn(makeMockDomainObject('other'), new MouseEvent("click"));
expect(mockScope.$apply).toHaveBeenCalled();
});

View File

@ -289,8 +289,9 @@ define([
});
it("notifies listeners when value is changed", function () {
treeView.value(mockDomainObject);
expect(mockCallback).toHaveBeenCalledWith(mockDomainObject);
treeView.value(mockDomainObject, {some: event});
expect(mockCallback)
.toHaveBeenCalledWith(mockDomainObject, {some: event});
});
it("does not notify listeners when deactivated", function () {

View File

@ -25,7 +25,6 @@ define([
'legacyRegistry',
'uuid',
'./api/api',
'text!./adapter/templates/edit-object-replacement.html',
'./selection/Selection',
'./api/objects/object-utils',
'./ui/ViewRegistry'
@ -34,7 +33,6 @@ define([
legacyRegistry,
uuid,
api,
editObjectTemplate,
Selection,
objectUtils,
ViewRegistry

View File

@ -1,46 +0,0 @@
<div class="abs l-flex-col" ng-controller="EditObjectController as EditObjectController">
<div mct-before-unload="EditObjectController.getUnloadWarning()"
class="holder flex-elem l-flex-row object-browse-bar ">
<div class="items-select left flex-elem l-flex-row grows">
<mct-representation key="'back-arrow'"
mct-object="domainObject"
class="flex-elem l-back"></mct-representation>
<mct-representation key="'object-header'"
mct-object="domainObject"
class="l-flex-row flex-elem grows object-header">
</mct-representation>
</div>
<div class="btn-bar right l-flex-row flex-elem flex-justify-end flex-fixed">
<mct-representation key="'switcher'"
mct-object="domainObject"
ng-model="representation">
</mct-representation>
<!-- Temporarily, on mobile, the action buttons are hidden-->
<mct-representation key="'action-group'"
mct-object="domainObject"
parameters="{ category: 'view-control' }"
class="mobile-hide">
</mct-representation>
</div>
</div>
<div class="holder l-flex-col flex-elem grows l-object-wrapper">
<div class="holder l-flex-col flex-elem grows l-object-wrapper-inner">
<!-- Toolbar and Save/Cancel buttons -->
<div class="l-edit-controls flex-elem l-flex-row flex-align-end">
<mct-representation key="'adapted-view-TOOLBAR'"
mct-object="domainObject"
class="flex-elem grows">
</mct-representation>
<mct-representation key="'edit-action-buttons'"
mct-object="domainObject"
class='flex-elem conclude-editing'>
</mct-representation>
</div>
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll"
toolbar="toolbar">
</mct-representation>
</div><!--/ l-object-wrapper-inner -->
</div>
</div>