Compare commits

...

3 Commits

Author SHA1 Message Date
a515ccfb61 [Edit Mode] Refactor edit concerns out of browse controllers
Fixed jslint errors

Removed commented code and added line breaks
2016-02-05 10:00:01 -08:00
560897fe81 [Edit Mode] #627 slightly modified edit representer to detect newly created objects
Added some comments, renamed controller variable in markup

Removed edit references from BrowseController
2016-02-04 16:59:12 -08:00
e1d20e7275 [Edit Mode] #627 remove edit concerns from browse controller 2016-02-04 16:59:11 -08:00
20 changed files with 402 additions and 240 deletions

View File

@ -80,7 +80,6 @@ define([
"$scope", "$scope",
"$route", "$route",
"$location", "$location",
"$q",
"objectService", "objectService",
"navigationService", "navigationService",
"urlService" "urlService"
@ -202,6 +201,10 @@ define([
{ {
"key": "inspector-region", "key": "inspector-region",
"templateUrl": "templates/browse/inspector-region.html" "templateUrl": "templates/browse/inspector-region.html"
},
{
"key": "view-region",
"templateUrl": "templates/view-region.html"
} }
], ],
"services": [ "services": [

View File

@ -47,11 +47,6 @@
<div class="holder l-flex-col flex-elem grows l-object-wrapper-inner"> <div class="holder l-flex-col flex-elem grows l-object-wrapper-inner">
<!-- Toolbar and Save/Cancel buttons --> <!-- Toolbar and Save/Cancel buttons -->
<div class="l-edit-controls flex-elem l-flex-row flex-align-end"> <div class="l-edit-controls flex-elem l-flex-row flex-align-end">
<mct-toolbar name="mctToolbar"
structure="toolbar.structure"
ng-model="toolbar.state"
class="flex-elem grows">
</mct-toolbar>
<mct-representation key="'edit-action-buttons'" <mct-representation key="'edit-action-buttons'"
mct-object="domainObject" mct-object="domainObject"
class='flex-elem conclude-editing'> class='flex-elem conclude-editing'>

View File

@ -63,7 +63,7 @@
<mct-split-pane class='l-object-and-inspector contents abs' anchor='right'> <mct-split-pane class='l-object-and-inspector contents abs' anchor='right'>
<div class='split-pane-component t-object pane primary-pane left'> <div class='split-pane-component t-object pane primary-pane left'>
<mct-representation mct-object="navigatedObject" <mct-representation mct-object="navigatedObject"
key="'browse-object'" key="'view-region'"
class="abs holder holder-object"> class="abs holder holder-object">
</mct-representation> </mct-representation>
</div> </div>
@ -87,4 +87,3 @@
</div> </div>
<mct-include key="'bottombar'"></mct-include> <mct-include key="'bottombar'"></mct-include>
</div> </div>

View File

@ -19,14 +19,12 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div content="jquery-wrapper" <div ng-controller="RegionController as regionController">
class="abs holder-all edit-mode" <div ng-repeat="part in regions.view.parts">
ng-controller="EditController as editMode" <mct-representation
mct-before-unload="editMode.getUnloadWarning()"> key="part.content.key"
mct-object="domainObject"
<mct-representation key="'edit-object'" mct-object="editMode.navigatedObject()"> ng-model="ngModel">
</mct-representation> </mct-representation>
<mct-include key="'bottombar'"></mct-include>
</div> </div>
</div><!--/ PaneController -->

View File

@ -27,10 +27,9 @@
*/ */
define( define(
[ [
'../../../representation/src/gestures/GestureConstants', '../../../representation/src/gestures/GestureConstants'
'../../edit/src/objects/EditableDomainObject'
], ],
function (GestureConstants, EditableDomainObject) { function (GestureConstants) {
"use strict"; "use strict";
var ROOT_ID = "ROOT", var ROOT_ID = "ROOT",
@ -47,18 +46,11 @@ define(
* @memberof platform/commonUI/browse * @memberof platform/commonUI/browse
* @constructor * @constructor
*/ */
function BrowseController($scope, $route, $location, $q, objectService, navigationService, urlService) { function BrowseController($scope, $route, $location, objectService, navigationService, urlService) {
var path = [ROOT_ID].concat( var path = [ROOT_ID].concat(
($route.current.params.ids || DEFAULT_PATH).split("/") ($route.current.params.ids || DEFAULT_PATH).split("/")
); );
function isDirty(){
var editorCapability = $scope.navigatedObject &&
$scope.navigatedObject.getCapability("editor"),
hasChanges = editorCapability && editorCapability.dirty();
return hasChanges;
}
function updateRoute(domainObject) { function updateRoute(domainObject) {
var priorRoute = $route.current, var priorRoute = $route.current,
// Act as if params HADN'T changed to avoid page reload // Act as if params HADN'T changed to avoid page reload
@ -75,9 +67,7 @@ define(
// urlService.urlForLocation used to adjust current // urlService.urlForLocation used to adjust current
// path to new, addressed, path based on // path to new, addressed, path based on
// domainObject // domainObject
$location.path(urlService.urlForLocation("browse", $location.path(urlService.urlForLocation("browse", domainObject));
domainObject.hasCapability('editor') ?
domainObject.getOriginalObject() : domainObject));
} }
@ -89,17 +79,15 @@ define(
return; return;
} }
if (isDirty() && !confirm(CONFIRM_MSG)) { if (navigationService.setNavigation(domainObject)) {
$scope.treeModel.selectedObject = $scope.navigatedObject;
navigationService.setNavigation($scope.navigatedObject);
} else {
if ($scope.navigatedObject && $scope.navigatedObject.hasCapability("editor")){
$scope.navigatedObject.getCapability("editor").cancel();
}
$scope.navigatedObject = domainObject; $scope.navigatedObject = domainObject;
$scope.treeModel.selectedObject = domainObject; $scope.treeModel.selectedObject = domainObject;
navigationService.setNavigation(domainObject);
updateRoute(domainObject); updateRoute(domainObject);
} else {
//If navigation was unsuccessful (ie. blocked), reset
// the selected object in the tree to the currently
// navigated object
$scope.treeModel.selectedObject = $scope.navigatedObject ;
} }
} }
@ -170,16 +158,11 @@ define(
selectedObject: navigationService.getNavigation() selectedObject: navigationService.getNavigation()
}; };
$scope.beforeUnloadWarning = function() {
return isDirty() ?
"Unsaved changes will be lost if you leave this page." :
undefined;
};
// Listen for changes in navigation state. // Listen for changes in navigation state.
navigationService.addListener(setNavigation); navigationService.addListener(setNavigation);
// Also listen for changes which come from the tree // 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", setNavigation); $scope.$watch("treeModel.selectedObject", setNavigation);
// Clean up when the scope is destroyed // Clean up when the scope is destroyed
@ -191,4 +174,3 @@ define(
return BrowseController; return BrowseController;
} }
); );

View File

@ -22,11 +22,8 @@
/*global define,Promise*/ /*global define,Promise*/
define( define(
[ [],
'../../../representation/src/gestures/GestureConstants', function () {
'../../edit/src/objects/EditableDomainObject'
],
function (GestureConstants, EditableDomainObject) {
"use strict"; "use strict";
/** /**
@ -57,10 +54,9 @@ define(
function updateQueryParam(viewKey) { function updateQueryParam(viewKey) {
var unlisten, var unlisten,
priorRoute = $route.current, priorRoute = $route.current;
isEditMode = $scope.domainObject && $scope.domainObject.hasCapability('editor');
if (viewKey && !isEditMode) { if (viewKey) {
$location.search('view', viewKey); $location.search('view', viewKey);
unlisten = $scope.$on('$locationChangeSuccess', function () { unlisten = $scope.$on('$locationChangeSuccess', function () {
// Checks path to make sure /browse/ is at front // Checks path to make sure /browse/ is at front
@ -76,10 +72,6 @@ define(
$scope.$watch('domainObject', setViewForDomainObject); $scope.$watch('domainObject', setViewForDomainObject);
$scope.$watch('representation.selected.key', updateQueryParam); $scope.$watch('representation.selected.key', updateQueryParam);
$scope.cancelEditing = function() {
navigationService.setNavigation($scope.domainObject.getDomainObject());
};
$scope.doAction = function (action){ $scope.doAction = function (action){
return $scope[action] && $scope[action](); return $scope[action] && $scope[action]();
}; };
@ -89,4 +81,3 @@ define(
return BrowseObjectController; return BrowseObjectController;
} }
); );

View File

@ -23,9 +23,10 @@
define( define(
[ [
'./InspectorRegion' './InspectorRegion',
'./ViewRegion'
], ],
function (InspectorRegion) { function (InspectorRegion, ViewRegion) {
"use strict"; "use strict";
/** /**
@ -53,6 +54,7 @@ define(
var regions = type.getDefinition().regions || {}; var regions = type.getDefinition().regions || {};
regions.inspector = regions.inspector || new InspectorRegion(); regions.inspector = regions.inspector || new InspectorRegion();
regions.view = regions.view || new ViewRegion();
type.getDefinition().regions = regions; type.getDefinition().regions = regions;

View File

@ -0,0 +1,73 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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.
*****************************************************************************/
/*global define,window*/
define(
[
'../../regions/src/Region'
],
function (Region) {
"use strict";
/**
* Defines the default View region. Captured in a class to
* allow for modular extension and customization of regions based on
* the typical case.
* @memberOf platform/commonUI/regions
* @constructor
*/
function ViewRegion() {
Region.call(this);
this.buildRegion();
}
ViewRegion.prototype = Object.create(Region.prototype);
ViewRegion.prototype.constructor = Region;
/**
* @private
*/
ViewRegion.prototype.buildRegion = function() {
var browseViewPart = {
name: 'browse-view',
title: 'Browse Object View',
modes: ['browse'],
content: {
key: 'browse-object'
}
},
editViewPart = {
name: 'edit-view',
title: 'Edit Object View',
modes: ['edit'],
content: {
key: 'edit-object'
}
};
this.addPart(browseViewPart);
this.addPart(editViewPart);
};
return ViewRegion;
}
);

View File

@ -37,7 +37,7 @@ define(
*/ */
function NavigationService() { function NavigationService() {
this.navigated = undefined; this.navigated = undefined;
this.callbacks = []; this.callbacks = {};
} }
/** /**
@ -50,26 +50,48 @@ define(
/** /**
* Set the current navigation state. This will invoke listeners. * Set the current navigation state. This will invoke listeners.
* Changing the navigation state will be blocked if any of the
* 'before' navigation state change listeners return 'false'.
* @param {DomainObject} domainObject the domain object to navigate to * @param {DomainObject} domainObject the domain object to navigate to
*/ */
NavigationService.prototype.setNavigation = function (value) { NavigationService.prototype.setNavigation = function (value) {
var canNavigate = true;
if (this.navigated !== value) { if (this.navigated !== value) {
canNavigate = (this.callbacks.before || [])
.reduce(function (previous, callback) {
//Check whether the callback returned a value of
// 'false' indicating that navigation should not
// continue. All other return values will allow
// navigation to continue
return (callback(value)!==false) && previous;
}, true);
if (canNavigate) {
this.navigated = value; this.navigated = value;
this.callbacks.forEach(function (callback) { (this.callbacks.after || []).forEach(function (callback) {
callback(value); callback(value);
}); });
} }
}
return canNavigate;
}; };
/** /**
* Listen for changes in navigation. The passed callback will * Listen for changes in navigation. The passed callback will
* be invoked with the new domain object of navigation when * be invoked with the new domain object of navigation when
* this changes. * this changes. Callbacks can be registered to listen to pre or
* post-navigation events. The event to listen to is specified using
* the event parameter. In the case of pre-navigation events
* returning a false value will prevent the navigation event from
* going ahead.
* @param {function} callback the callback to invoke when * @param {function} callback the callback to invoke when
* navigation state changes * navigation state changes
* @param {string} [event=after] the navigation event to listen to.
* One of 'before' or 'after'.
*/ */
NavigationService.prototype.addListener = function (callback) { NavigationService.prototype.addListener = function (callback, event) {
this.callbacks.push(callback); event = event || 'after';
this.callbacks[event] = this.callbacks[event] || [];
this.callbacks[event].push(callback);
}; };
/** /**
@ -77,9 +99,12 @@ define(
* @param {function} callback the callback which should * @param {function} callback the callback which should
* no longer be invoked when navigation state * no longer be invoked when navigation state
* changes * changes
* @param {string} [event=after] the navigation event that the
* callback is registered to. One of 'before' or 'after'.
*/ */
NavigationService.prototype.removeListener = function (callback) { NavigationService.prototype.removeListener = function (callback, event) {
this.callbacks = this.callbacks.filter(function (cb) { event = event || 'after';
this.callbacks[event] = this.callbacks[event].filter(function (cb) {
return cb !== callback; return cb !== callback;
}); });
}; };

View File

@ -29,8 +29,7 @@ define(
function (BrowseController) { function (BrowseController) {
"use strict"; "use strict";
//TODO: Disabled for NEM Beta describe("The browse controller", function () {
xdescribe("The browse controller", function () {
var mockScope, var mockScope,
mockRoute, mockRoute,
mockLocation, mockLocation,
@ -230,7 +229,10 @@ define(
// prior to setting $route.current // prior to setting $route.current
mockLocation.path.andReturn("/browse/"); mockLocation.path.andReturn("/browse/");
mockNavigationService.setNavigation.andReturn(true);
// Exercise the Angular workaround // Exercise the Angular workaround
mockNavigationService.addListener.mostRecentCall.args[0]();
mockScope.$on.mostRecentCall.args[1](); mockScope.$on.mostRecentCall.args[1]();
expect(mockUnlisten).toHaveBeenCalled(); expect(mockUnlisten).toHaveBeenCalled();
@ -241,6 +243,32 @@ define(
); );
}); });
it("after successful navigation event sets the selected tree " +
"object", function () {
mockScope.navigatedObject = mockDomainObject;
mockNavigationService.setNavigation.andReturn(true);
//Simulate a change in selected tree object
mockScope.treeModel = {selectedObject: mockDomainObject};
mockScope.$watch.mostRecentCall.args[1](mockNextObject);
expect(mockScope.treeModel.selectedObject).toBe(mockNextObject);
expect(mockScope.treeModel.selectedObject).not.toBe(mockDomainObject);
});
it("after failed navigation event resets the selected tree" +
" object", function () {
mockScope.navigatedObject = mockDomainObject;
mockNavigationService.setNavigation.andReturn(false);
//Simulate a change in selected tree object
mockScope.treeModel = {selectedObject: mockDomainObject};
mockScope.$watch.mostRecentCall.args[1](mockNextObject);
expect(mockScope.treeModel.selectedObject).not.toBe(mockNextObject);
expect(mockScope.treeModel.selectedObject).toBe(mockDomainObject);
});
}); });
} }
); );

View File

@ -84,6 +84,24 @@ define(
expect(callback).not.toHaveBeenCalled(); expect(callback).not.toHaveBeenCalled();
}); });
it("adds listeners to the 'after' state by default", function(){
expect(navigationService.callbacks.after).toBeUndefined();
navigationService.addListener(function(){});
expect(navigationService.callbacks.after).toBeDefined();
expect(navigationService.callbacks.after.length).toBe(1);
});
it("allows navigationService events to be prevented", function(){
var callback = jasmine.createSpy("callback"),
navigationResult;
callback.andReturn(false);
navigationService.addListener(callback, "before");
navigationResult = navigationService.setNavigation({});
expect(callback).toHaveBeenCalled();
expect(navigationResult).toBe(false);
});
}); });
} }
); );

View File

@ -22,10 +22,10 @@
/*global define*/ /*global define*/
define([ define([
"./src/controllers/EditController",
"./src/controllers/EditActionController", "./src/controllers/EditActionController",
"./src/controllers/EditPanesController", "./src/controllers/EditPanesController",
"./src/controllers/ElementsController", "./src/controllers/ElementsController",
"./src/controllers/EditObjectController",
"./src/directives/MCTBeforeUnload", "./src/directives/MCTBeforeUnload",
"./src/actions/LinkAction", "./src/actions/LinkAction",
"./src/actions/EditAction", "./src/actions/EditAction",
@ -38,10 +38,10 @@ define([
"./src/representers/EditToolbarRepresenter", "./src/representers/EditToolbarRepresenter",
'legacyRegistry' 'legacyRegistry'
], function ( ], function (
EditController,
EditActionController, EditActionController,
EditPanesController, EditPanesController,
ElementsController, ElementsController,
EditObjectController,
MCTBeforeUnload, MCTBeforeUnload,
LinkAction, LinkAction,
EditAction, EditAction,
@ -58,22 +58,7 @@ define([
legacyRegistry.register("platform/commonUI/edit", { legacyRegistry.register("platform/commonUI/edit", {
"extensions": { "extensions": {
"routes": [
{
"when": "/edit",
"templateUrl": "templates/edit.html"
}
],
"controllers": [ "controllers": [
{
"key": "EditController",
"implementation": EditController,
"depends": [
"$scope",
"$q",
"navigationService"
]
},
{ {
"key": "EditActionController", "key": "EditActionController",
"implementation": EditActionController, "implementation": EditActionController,
@ -94,6 +79,14 @@ define([
"depends": [ "depends": [
"$scope" "$scope"
] ]
},
{
"key": "EditObjectController",
"implementation": EditObjectController,
"depends": [
"$scope",
"$location"
]
} }
], ],
"directives": [ "directives": [
@ -101,7 +94,8 @@ define([
"key": "mctBeforeUnload", "key": "mctBeforeUnload",
"implementation": MCTBeforeUnload, "implementation": MCTBeforeUnload,
"depends": [ "depends": [
"$window" "$window",
"navigationService"
] ]
} }
], ],
@ -197,6 +191,9 @@ define([
"templateUrl": "templates/edit-object.html", "templateUrl": "templates/edit-object.html",
"uses": [ "uses": [
"view" "view"
],
"gestures": [
"drop"
] ]
}, },
{ {

View File

@ -19,50 +19,51 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<mct-representation key="'topbar-edit'" <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" mct-object="domainObject"
ng-model="representation"> ng-model="representation">
</mct-representation> </mct-representation>
<div class="holder edit-area abs"> <!-- Temporarily, on mobile, the action buttons are hidden-->
<mct-split-pane class='contents abs' anchor='right'> <mct-representation key="'action-group'"
<div class='split-pane-component pane left edit-main'> 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-toolbar name="mctToolbar" <mct-toolbar name="mctToolbar"
structure="toolbar.structure" structure="toolbar.structure"
ng-model="toolbar.state"> ng-model="toolbar.state"
class="flex-elem grows">
</mct-toolbar> </mct-toolbar>
<mct-representation key="'edit-action-buttons'"
mct-object="domainObject"
class='flex-elem conclude-editing'>
</mct-representation>
</div>
<mct-representation key="representation.selected.key" <mct-representation key="representation.selected.key"
toolbar="toolbar"
mct-object="representation.selected.key && domainObject" mct-object="representation.selected.key && domainObject"
class="holder abs object-holder work-area"> class="abs flex-elem grows object-holder-main scroll"
toolbar="toolbar">
</mct-representation> </mct-representation>
</div><!--/ l-object-wrapper-inner -->
</div> </div>
<mct-splitter></mct-splitter>
<div
class='split-pane-component pane right edit-objects menus-to-left'
ng-controller='EditPanesController as editPanes'
>
<mct-split-pane class='contents abs' anchor='bottom'>
<div
class="abs pane top accordion"
ng-controller="ToggleController as toggle"
>
<mct-container key="accordion" label="Library">
<mct-representation key="'tree'"
mct-object="editPanes.getRoot()">
</mct-representation>
</mct-container>
</div>
<mct-splitter></mct-splitter>
<div
class="abs pane bottom accordion"
ng-controller="ToggleController as toggle"
>
<mct-container key="accordion" label="Elements">
<mct-representation key="'edit-elements'" mct-object="domainObject">
</mct-representation>
</mct-container>
</div>
</mct-split-pane>
</div>
</mct-split-pane>
</div> </div>

View File

@ -72,13 +72,26 @@ define(
* Enter edit mode. * Enter edit mode.
*/ */
EditAction.prototype.perform = function () { EditAction.prototype.perform = function () {
var editableObject; var self = this;
if (!this.domainObject.hasCapability("editor")) { if (!this.domainObject.hasCapability("editor")) {
editableObject = new EditableDomainObject(this.domainObject, this.$q); //TODO: This is only necessary because the drop gesture is
editableObject.getCapability('status').set('editing', true); // wrapping the object itself, need to refactor this later.
this.navigationService.setNavigation(editableObject); // All responsibility for switching into edit mode should be
// in the edit action, and not duplicated in the gesture
this.domainObject = new EditableDomainObject(this.domainObject, this.$q);
} }
//this.$location.path("/edit"); this.navigationService.setNavigation(this.domainObject);
this.domainObject.getCapability('status').set('editing', true);
//Register a listener to automatically cancel this edit action
//if the user navigates away from this object.
function cancelEditing(navigatedTo){
if (!navigatedTo || navigatedTo.getId() !== self.domainObject.getId()) {
self.domainObject.getCapability('editor').cancel();
self.navigationService.removeListener(cancelEditing);
}
}
this.navigationService.addListener(cancelEditing);
}; };
/** /**

View File

@ -26,41 +26,44 @@
* @namespace platform/commonUI/edit * @namespace platform/commonUI/edit
*/ */
define( define(
["../objects/EditableDomainObject"], [],
function (EditableDomainObject) { function () {
"use strict"; "use strict";
/** /**
* Controller which is responsible for populating the scope for * Controller which is responsible for populating the scope for
* Edit mode; introduces an editable version of the currently * Edit mode
* navigated domain object into the scope.
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @constructor * @constructor
*/ */
function EditController($scope, $q, navigationService) { function EditObjectController($scope, $location) {
var self = this; this.scope = $scope;
function setNavigation(domainObject) { var navigatedObject;
// Wrap the domain object such that all mutation is function setViewForDomainObject(domainObject) {
// confined to edit mode (until Save)
self.navigatedDomainObject = var locationViewKey = $location.search().view;
domainObject && new EditableDomainObject(domainObject, $q);
function selectViewIfMatching(view) {
if (view.key === locationViewKey) {
$scope.representation = $scope.representation || {};
$scope.representation.selected = view;
}
} }
setNavigation(navigationService.getNavigation()); if (locationViewKey) {
navigationService.addListener(setNavigation); ((domainObject && domainObject.useCapability('view')) || [])
$scope.$on("$destroy", function () { .forEach(selectViewIfMatching);
navigationService.removeListener(setNavigation); }
}); navigatedObject = domainObject;
} }
/** $scope.$watch('domainObject', setViewForDomainObject);
* Get the domain object which is navigated-to.
* @returns {DomainObject} the domain object that is navigated-to $scope.doAction = function (action){
*/ return $scope[action] && $scope[action]();
EditController.prototype.navigatedObject = function () {
return this.navigatedDomainObject;
}; };
}
/** /**
* Get the warning to show if the user attempts to navigate * Get the warning to show if the user attempts to navigate
@ -68,17 +71,20 @@ define(
* @returns {string} the warning to show, or undefined if * @returns {string} the warning to show, or undefined if
* there are no unsaved changes * there are no unsaved changes
*/ */
EditController.prototype.getUnloadWarning = function () { EditObjectController.prototype.getUnloadWarning = function () {
var navigatedObject = this.navigatedDomainObject, var navigatedObject = this.scope.domainObject,
editorCapability = navigatedObject && editorCapability = navigatedObject &&
navigatedObject.getCapability("editor"), navigatedObject.getCapability("editor"),
hasChanges = editorCapability && editorCapability.dirty(); statusCapability = navigatedObject &&
navigatedObject.getCapability("status"),
hasChanges = statusCapability && statusCapability.get('editing')
&& editorCapability && editorCapability.dirty();
return hasChanges ? return hasChanges ?
"Unsaved changes will be lost if you leave this page." : "Unsaved changes will be lost if you leave this page." :
undefined; undefined;
}; };
return EditController; return EditObjectController;
} }
); );

View File

@ -35,7 +35,7 @@ define(
* @constructor * @constructor
* @param $window the window * @param $window the window
*/ */
function MCTBeforeUnload($window) { function MCTBeforeUnload($window, navigationService) {
var unloads = [], var unloads = [],
oldBeforeUnload = $window.onbeforeunload; oldBeforeUnload = $window.onbeforeunload;
@ -55,8 +55,23 @@ define(
return scope.$eval(attrs.mctBeforeUnload); return scope.$eval(attrs.mctBeforeUnload);
} }
function shouldAllowNavigation(){
// Get an unload message (if any)
var warning = unload();
// Prompt the user if there's an unload message
return !warning || $window.confirm(warning);
}
// Show a dialog before allowing a location change
function checkNavigationEvent(event) {
// Return a false value to the navigationService to
// indicate that the navigation event should be prevented
return shouldAllowNavigation();
}
// Stop using this unload expression // Stop using this unload expression
function removeUnload() { function removeUnload() {
navigationService.removeListener(checkNavigationEvent, "before");
unloads = unloads.filter(function (callback) { unloads = unloads.filter(function (callback) {
return callback !== unload; return callback !== unload;
}); });
@ -67,11 +82,8 @@ define(
// Show a dialog before allowing a location change // Show a dialog before allowing a location change
function checkLocationChange(event) { function checkLocationChange(event) {
// Get an unload message (if any) if (!shouldAllowNavigation()) {
var warning = unload(); // Prevent the route change if it was confirmed
// 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(); event.preventDefault();
} }
} }
@ -90,6 +102,8 @@ define(
// Also handle route changes // Also handle route changes
scope.$on("$locationChangeStart", checkLocationChange); scope.$on("$locationChangeStart", checkLocationChange);
navigationService.addListener(checkNavigationEvent, "before");
} }
return { return {

View File

@ -22,89 +22,72 @@
/*global define,describe,it,expect,beforeEach,jasmine*/ /*global define,describe,it,expect,beforeEach,jasmine*/
define( define(
["../../src/controllers/EditController"], ["../../src/controllers/EditObjectController"],
function (EditController) { function (EditObjectController) {
"use strict"; "use strict";
describe("The Edit mode controller", function () { describe("The Edit mode controller", function () {
var mockScope, var mockScope,
mockQ,
mockNavigationService,
mockObject, mockObject,
mockType, mockType,
mockLocation,
mockStatusCapability,
mockCapabilities,
controller; 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 () { beforeEach(function () {
mockScope = jasmine.createSpyObj( mockScope = jasmine.createSpyObj(
"$scope", "$scope",
[ "$on" ] [ "$on", "$watch" ]
);
mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[ "getNavigation", "addListener", "removeListener" ]
); );
mockObject = jasmine.createSpyObj( mockObject = jasmine.createSpyObj(
"domainObject", "domainObject",
[ "getId", "getModel", "getCapability", "hasCapability" ] [ "getId", "getModel", "getCapability", "hasCapability", "useCapability" ]
); );
mockType = jasmine.createSpyObj( mockType = jasmine.createSpyObj(
"type", "type",
[ "hasFeature" ] [ "hasFeature" ]
); );
mockStatusCapability = jasmine.createSpyObj('statusCapability',
["get"]
);
mockCapabilities = {
"type" : mockType,
"status": mockStatusCapability
};
mockLocation = jasmine.createSpyObj('$location',
["search"]
);
mockLocation.search.andReturn({"view": "fixed"});
mockNavigationService.getNavigation.andReturn(mockObject);
mockObject.getId.andReturn("test"); mockObject.getId.andReturn("test");
mockObject.getModel.andReturn({ name: "Test object" }); mockObject.getModel.andReturn({ name: "Test object" });
mockObject.getCapability.andCallFake(function (key) { mockObject.getCapability.andCallFake(function (key) {
return key === 'type' && mockType; return mockCapabilities[key];
}); });
mockType.hasFeature.andReturn(true); mockType.hasFeature.andReturn(true);
controller = new EditController( mockScope.domainObject = mockObject;
controller = new EditObjectController(
mockScope, mockScope,
mockQ, mockLocation
mockNavigationService
); );
}); });
it("exposes the currently-navigated object", function () {
expect(controller.navigatedObject()).toBeDefined();
expect(controller.navigatedObject().getId()).toEqual("test");
});
it("adds an editor capability to the navigated object", function () {
// Should provide an editor capability...
expect(controller.navigatedObject().getCapability("editor"))
.toBeDefined();
// Shouldn't have been the mock capability we provided
expect(controller.navigatedObject().getCapability("editor"))
.not.toEqual(mockType);
});
it("detaches its navigation listener when destroyed", function () {
var navCallback = mockNavigationService
.addListener.mostRecentCall.args[0];
expect(mockScope.$on).toHaveBeenCalledWith(
"$destroy",
jasmine.any(Function)
);
// Verify precondition
expect(mockNavigationService.removeListener)
.not.toHaveBeenCalled();
// Trigger destroy
mockScope.$on.mostRecentCall.args[1]();
// Listener should have been removed
expect(mockNavigationService.removeListener)
.toHaveBeenCalledWith(navCallback);
});
it("exposes a warning message for unload", function () { it("exposes a warning message for unload", function () {
var obj = controller.navigatedObject(), var obj = mockObject,
mockEditor = jasmine.createSpyObj('editor', ['dirty']); mockEditor = jasmine.createSpyObj('editor', ['dirty']);
// Normally, should be undefined // Normally, should be undefined
@ -112,14 +95,32 @@ define(
// Override the object's editor capability, make it look // Override the object's editor capability, make it look
// like there are unsaved changes. // like there are unsaved changes.
obj.getCapability = jasmine.createSpy(); mockCapabilities.editor = mockEditor;
obj.getCapability.andReturn(mockEditor);
mockEditor.dirty.andReturn(true); mockEditor.dirty.andReturn(true);
mockStatusCapability.get.andReturn(true);
// Should have some warning message here now // Should have some warning message here now
expect(controller.getUnloadWarning()).toEqual(jasmine.any(String)); expect(controller.getUnloadWarning()).toEqual(jasmine.any(String));
}); });
it("sets the active view from query parameters", function () {
var 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);
expect(mockScope.representation.selected)
.toEqual(testViews[1]);
});
}); });
} }
); );

View File

@ -31,6 +31,7 @@ define(
mockScope, mockScope,
testAttrs, testAttrs,
mockEvent, mockEvent,
mockNavigationService,
directive; directive;
function fireListener(eventType, value) { function fireListener(eventType, value) {
@ -46,7 +47,8 @@ define(
mockScope = jasmine.createSpyObj("$scope", ['$eval', '$on']); mockScope = jasmine.createSpyObj("$scope", ['$eval', '$on']);
testAttrs = { mctBeforeUnload: "someExpression" }; testAttrs = { mctBeforeUnload: "someExpression" };
mockEvent = jasmine.createSpyObj("event", ["preventDefault"]); mockEvent = jasmine.createSpyObj("event", ["preventDefault"]);
directive = new MCTBeforeUnload(mockWindow); mockNavigationService = jasmine.createSpyObj("navigationService", ["addListener", "removeListener"]);
directive = new MCTBeforeUnload(mockWindow, mockNavigationService);
directive.link(mockScope, {}, testAttrs); directive.link(mockScope, {}, testAttrs);
}); });
@ -65,6 +67,10 @@ define(
); );
}); });
it("listens for navigation changes", function () {
expect(mockNavigationService.addListener).toHaveBeenCalledWith(jasmine.any(Function), "before");
});
it("listens for its scope's destroy event", function () { it("listens for its scope's destroy event", function () {
expect(mockScope.$on).toHaveBeenCalledWith( expect(mockScope.$on).toHaveBeenCalledWith(
"$destroy", "$destroy",
@ -108,6 +114,7 @@ define(
it("cleans up listeners when destroyed", function () { it("cleans up listeners when destroyed", function () {
fireListener("$destroy", mockEvent); fireListener("$destroy", mockEvent);
expect(mockWindow.onbeforeunload).toBeUndefined(); expect(mockWindow.onbeforeunload).toBeUndefined();
expect(mockNavigationService.removeListener).toHaveBeenCalled();
}); });

View File

@ -33,6 +33,8 @@ define(
testRepresentation, testRepresentation,
mockDomainObject, mockDomainObject,
mockPersistence, mockPersistence,
mockStatusCapability,
mockCapabilities,
representer; representer;
function mockPromise(value) { function mockPromise(value) {
@ -57,11 +59,20 @@ define(
]); ]);
mockPersistence = mockPersistence =
jasmine.createSpyObj("persistence", ["persist"]); jasmine.createSpyObj("persistence", ["persist"]);
mockStatusCapability =
jasmine.createSpyObj("statusCapability", ["get", "listen"]);
mockCapabilities = {
"persistence": mockPersistence,
"status": mockStatusCapability
};
mockDomainObject.getModel.andReturn({}); mockDomainObject.getModel.andReturn({});
mockDomainObject.hasCapability.andReturn(true); mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.useCapability.andReturn(true); mockDomainObject.useCapability.andReturn(true);
mockDomainObject.getCapability.andReturn(mockPersistence); mockDomainObject.getCapability.andCallFake(function(capability){
return mockCapabilities[capability];
});
representer = new EditRepresenter(mockQ, mockLog, mockScope); representer = new EditRepresenter(mockQ, mockLog, mockScope);
representer.represent(testRepresentation, mockDomainObject); representer.represent(testRepresentation, mockDomainObject);

View File

@ -165,16 +165,14 @@ define(
if (shouldCreateVirtualPanel(domainObject, selectedObject)){ if (shouldCreateVirtualPanel(domainObject, selectedObject)){
editableDomainObject = createVirtualPanel(domainObject, selectedObject); editableDomainObject = createVirtualPanel(domainObject, selectedObject);
if (editableDomainObject) { if (editableDomainObject) {
navigationService.setNavigation(editableDomainObject); editableDomainObject.getCapability('action').perform('edit');
broadcastDrop(id, event); broadcastDrop(id, event);
editableDomainObject.getCapability('status').set('editing', true);
} }
} else { } else {
$q.when(action && action.perform()).then(function (result) { $q.when(action && action.perform()).then(function (result) {
//Don't go into edit mode for folders //Don't go into edit mode for folders
if (domainObjectType!=='folder') { if (domainObjectType!=='folder') {
navigationService.setNavigation(editableDomainObject); editableDomainObject.getCapability('action').perform('edit');
editableDomainObject.getCapability('status').set('editing', true);
} }
broadcastDrop(id, event); broadcastDrop(id, event);
}); });