mirror of
https://github.com/nasa/openmct.git
synced 2025-06-15 13:48:12 +00:00
Merge branch 'open1035' into open-master
Merge WTD-1035; resolving conflicts to avoid WTD-1069 Conflicts: platform/commonUI/edit/src/controllers/EditController.js platform/commonUI/edit/src/objects/EditableDomainObjectCache.js
This commit is contained in:
@ -1,5 +1,26 @@
|
|||||||
Contains sources and resources associated with Edit mode.
|
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
|
# Toolbars
|
||||||
|
|
||||||
Views may specify the contents of a toolbar through a `toolbar`
|
Views may specify the contents of a toolbar through a `toolbar`
|
||||||
|
@ -23,6 +23,13 @@
|
|||||||
"depends": [ "$scope" ]
|
"depends": [ "$scope" ]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"directives": [
|
||||||
|
{
|
||||||
|
"key": "mctBeforeUnload",
|
||||||
|
"implementation": "directives/MCTBeforeUnload.js",
|
||||||
|
"depends": [ "$window" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"key": "edit",
|
"key": "edit",
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<div content="jquery-wrapper"
|
<div content="jquery-wrapper"
|
||||||
class="abs holder-all edit-mode"
|
class="abs holder-all edit-mode"
|
||||||
ng-controller="EditController">
|
ng-controller="EditController as editMode"
|
||||||
|
mct-before-unload="editMode.getUnloadWarning()">
|
||||||
|
|
||||||
<mct-representation key="'edit-object'" mct-object="navigatedObject">
|
<mct-representation key="'edit-object'" mct-object="editMode.navigatedObject()">
|
||||||
</mct-representation>
|
</mct-representation>
|
||||||
|
|
||||||
<mct-include key="'bottombar'"></mct-include>
|
<mct-include key="'bottombar'"></mct-include>
|
||||||
|
@ -77,6 +77,13 @@ define(
|
|||||||
*/
|
*/
|
||||||
cancel: function () {
|
cancel: function () {
|
||||||
return resolvePromise(undefined);
|
return resolvePromise(undefined);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Check if there are any unsaved changes.
|
||||||
|
* @returns {boolean} true if there are unsaved changes
|
||||||
|
*/
|
||||||
|
dirty: function () {
|
||||||
|
return cache.dirty();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -15,10 +15,12 @@ define(
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function EditController($scope, $q, navigationService) {
|
function EditController($scope, $q, navigationService) {
|
||||||
|
var navigatedObject;
|
||||||
|
|
||||||
function setNavigation(domainObject) {
|
function setNavigation(domainObject) {
|
||||||
// Wrap the domain object such that all mutation is
|
// Wrap the domain object such that all mutation is
|
||||||
// confined to edit mode (until Save)
|
// confined to edit mode (until Save)
|
||||||
$scope.navigatedObject =
|
navigatedObject =
|
||||||
domainObject && new EditableDomainObject(domainObject, $q);
|
domainObject && new EditableDomainObject(domainObject, $q);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,6 +29,31 @@ define(
|
|||||||
$scope.$on("$destroy", function () {
|
$scope.$on("$destroy", function () {
|
||||||
navigationService.removeListener(setNavigation);
|
navigationService.removeListener(setNavigation);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get the domain object which is navigated-to.
|
||||||
|
* @returns {DomainObject} the domain object that is navigated-to
|
||||||
|
*/
|
||||||
|
navigatedObject: function () {
|
||||||
|
return navigatedObject;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
getUnloadWarning: function () {
|
||||||
|
var editorCapability = navigatedObject &&
|
||||||
|
navigatedObject.getCapability("editor"),
|
||||||
|
hasChanges = editorCapability && editorCapability.dirty();
|
||||||
|
|
||||||
|
return hasChanges ?
|
||||||
|
"Unsaved changes will be lost if you leave this page." :
|
||||||
|
undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return EditController;
|
return EditController;
|
||||||
|
84
platform/commonUI/edit/src/directives/MCTBeforeUnload.js
Normal file
84
platform/commonUI/edit/src/directives/MCTBeforeUnload.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/*global define*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
@ -103,6 +103,13 @@ define(
|
|||||||
// Save; pass a nonrecursive flag to avoid looping
|
// Save; pass a nonrecursive flag to avoid looping
|
||||||
return object.getCapability('editor').save(true);
|
return object.getCapability('editor').save(true);
|
||||||
}));
|
}));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Check if any objects have been marked dirty in this cache.
|
||||||
|
* @returns {boolean} true if objects are dirty
|
||||||
|
*/
|
||||||
|
dirty: function () {
|
||||||
|
return Object.keys(dirty).length > 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -44,17 +44,17 @@ define(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("places the currently-navigated object in scope", function () {
|
it("exposes the currently-navigated object", function () {
|
||||||
expect(mockScope.navigatedObject).toBeDefined();
|
expect(controller.navigatedObject()).toBeDefined();
|
||||||
expect(mockScope.navigatedObject.getId()).toEqual("test");
|
expect(controller.navigatedObject().getId()).toEqual("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds an editor capability to the navigated object", function () {
|
it("adds an editor capability to the navigated object", function () {
|
||||||
// Should provide an editor capability...
|
// Should provide an editor capability...
|
||||||
expect(mockScope.navigatedObject.getCapability("editor"))
|
expect(controller.navigatedObject().getCapability("editor"))
|
||||||
.toBeDefined();
|
.toBeDefined();
|
||||||
// Shouldn't have been the mock capability we provided
|
// Shouldn't have been the mock capability we provided
|
||||||
expect(mockScope.navigatedObject.getCapability("editor"))
|
expect(controller.navigatedObject().getCapability("editor"))
|
||||||
.not.toEqual(mockCapability);
|
.not.toEqual(mockCapability);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,6 +79,23 @@ define(
|
|||||||
.toHaveBeenCalledWith(navCallback);
|
.toHaveBeenCalledWith(navCallback);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes a warning message for unload", function () {
|
||||||
|
var obj = controller.navigatedObject(),
|
||||||
|
mockEditor = jasmine.createSpyObj('editor', ['dirty']);
|
||||||
|
|
||||||
|
// Normally, should be undefined
|
||||||
|
expect(controller.getUnloadWarning()).toBeUndefined();
|
||||||
|
|
||||||
|
// Override the object's editor capability, make it look
|
||||||
|
// like there are unsaved changes.
|
||||||
|
obj.getCapability = jasmine.createSpy();
|
||||||
|
obj.getCapability.andReturn(mockEditor);
|
||||||
|
mockEditor.dirty.andReturn(true);
|
||||||
|
|
||||||
|
// Should have some warning message here now
|
||||||
|
expect(controller.getUnloadWarning()).toEqual(jasmine.any(String));
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
@ -0,0 +1,95 @@
|
|||||||
|
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
["../../src/directives/MCTBeforeUnload"],
|
||||||
|
function (MCTBeforeUnload) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -14,6 +14,7 @@
|
|||||||
"controllers/EditActionController",
|
"controllers/EditActionController",
|
||||||
"controllers/EditController",
|
"controllers/EditController",
|
||||||
"controllers/EditPanesController",
|
"controllers/EditPanesController",
|
||||||
|
"directives/MCTBeforeUnload",
|
||||||
"objects/EditableDomainObject",
|
"objects/EditableDomainObject",
|
||||||
"objects/EditableDomainObjectCache",
|
"objects/EditableDomainObjectCache",
|
||||||
"objects/EditableModelCache",
|
"objects/EditableModelCache",
|
||||||
|
Reference in New Issue
Block a user