Merge pull request #712 from nasa/open627_no_regions_master

[Edit Mode] #627 Remove edit-related concerns from Browse controllers
This commit is contained in:
Victor Woeltjen 2016-03-02 12:12:11 -08:00
commit 75178576dd
20 changed files with 416 additions and 301 deletions

View File

@ -111,10 +111,11 @@ define([
"$scope", "$scope",
"$route", "$route",
"$location", "$location",
"$q", "$window",
"objectService", "objectService",
"navigationService", "navigationService",
"urlService", "urlService",
"policyService",
"DEFAULT_PATH" "DEFAULT_PATH"
] ]
}, },
@ -134,9 +135,7 @@ define([
"depends": [ "depends": [
"$scope", "$scope",
"$location", "$location",
"$route", "$route"
"$q",
"navigationService"
] ]
}, },
{ {
@ -170,6 +169,10 @@ define([
} }
], ],
"representations": [ "representations": [
{
"key": "view-object",
"templateUrl": "templates/view-object.html"
},
{ {
"key": "browse-object", "key": "browse-object",
"template": browseObjectTemplate, "template": browseObjectTemplate,

View File

@ -47,11 +47,6 @@
<div ng-if="isEditable" class="holder l-flex-col flex-elem grows l-object-wrapper-inner"> <div ng-if="isEditable" 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-object'"
class="abs holder holder-object"> class="abs holder holder-object">
</mct-representation> </mct-representation>
</div> </div>

View File

@ -19,14 +19,15 @@
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" <!--
class="abs holder-all edit-mode" A representation that allows the 'View' region of an object view to change
ng-controller="EditController as editMode" dynamically (eg. between browse and edit modes). Values correspond to a
mct-before-unload="editMode.getUnloadWarning()"> representation key, and currently defaults to 'browse-object'.
<mct-representation key="'edit-object'" mct-object="editMode.navigatedObject()"> 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 holder-object">
</mct-representation> </mct-representation>
<mct-include key="'bottombar'"></mct-include>
</div>

View File

@ -27,14 +27,12 @@
*/ */
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";
CONFIRM_MSG = "Unsaved changes will be lost if you leave this page.";
/** /**
* The BrowseController is used to populate the initial scope in Browse * The BrowseController is used to populate the initial scope in Browse
@ -50,23 +48,17 @@ define(
$scope, $scope,
$route, $route,
$location, $location,
$q, $window,
objectService, objectService,
navigationService, navigationService,
urlService, urlService,
policyService,
defaultPath defaultPath
) { ) {
var path = [ROOT_ID].concat( var path = [ROOT_ID].concat(
($route.current.params.ids || defaultPath).split("/") ($route.current.params.ids || defaultPath).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
@ -83,31 +75,35 @@ 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));
} }
// Callback for updating the in-scope reference to the object // Callback for updating the in-scope reference to the object
// that is currently navigated-to. // that is currently navigated-to.
function setNavigation(domainObject) { function setNavigation(domainObject) {
var navigationAllowed = true;
if (domainObject === $scope.navigatedObject){ if (domainObject === $scope.navigatedObject){
//do nothing; //do nothing;
return; return;
} }
if (isDirty() && !confirm(CONFIRM_MSG)) { policyService.allow("navigation", $scope.navigatedObject, domainObject, function(message){
$scope.treeModel.selectedObject = $scope.navigatedObject; navigationAllowed = $window.confirm(message + "\r\n\r\n" +
navigationService.setNavigation($scope.navigatedObject); " Are you sure you want to continue?");
} else { });
if ($scope.navigatedObject && $scope.navigatedObject.hasCapability("editor")){
$scope.navigatedObject.getCapability("editor").cancel(); if (navigationAllowed) {
}
$scope.navigatedObject = domainObject; $scope.navigatedObject = domainObject;
$scope.treeModel.selectedObject = domainObject; $scope.treeModel.selectedObject = domainObject;
navigationService.setNavigation(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 ;
} }
} }
@ -184,16 +180,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

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";
/** /**
@ -35,7 +32,7 @@ define(
* @memberof platform/commonUI/browse * @memberof platform/commonUI/browse
* @constructor * @constructor
*/ */
function BrowseObjectController($scope, $location, $route, $q, navigationService) { function BrowseObjectController($scope, $location, $route) {
var navigatedObject; var navigatedObject;
function setViewForDomainObject(domainObject) { function setViewForDomainObject(domainObject) {
@ -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]();
}; };

View File

@ -59,6 +59,7 @@ define(
callback(value); callback(value);
}); });
} }
return true;
}; };
/** /**

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,
@ -40,6 +39,8 @@ define(
mockUrlService, mockUrlService,
mockDomainObject, mockDomainObject,
mockNextObject, mockNextObject,
mockWindow,
mockPolicyService,
testDefaultRoot, testDefaultRoot,
controller; controller;
@ -56,14 +57,25 @@ define(
mockScope, mockScope,
mockRoute, mockRoute,
mockLocation, mockLocation,
mockWindow,
mockObjectService, mockObjectService,
mockNavigationService, mockNavigationService,
mockUrlService, mockUrlService,
mockPolicyService,
testDefaultRoot testDefaultRoot
); );
} }
beforeEach(function () { beforeEach(function () {
mockWindow = jasmine.createSpyObj('$window', [
"confirm"
]);
mockWindow.confirm.andReturn(true);
mockPolicyService = jasmine.createSpyObj('policyService', [
'allow'
]);
testDefaultRoot = "some-root-level-domain-object"; testDefaultRoot = "some-root-level-domain-object";
mockScope = jasmine.createSpyObj( mockScope = jasmine.createSpyObj(
@ -214,7 +226,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();
@ -225,6 +240,36 @@ 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;
mockWindow.confirm.andReturn(false);
mockPolicyService.allow.andCallFake(function(category, object, context, callback){
callback("unsaved changes");
return 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

@ -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",
@ -34,9 +34,9 @@ define([
"./src/actions/SaveAction", "./src/actions/SaveAction",
"./src/actions/CancelAction", "./src/actions/CancelAction",
"./src/policies/EditActionPolicy", "./src/policies/EditActionPolicy",
"./src/policies/EditNavigationPolicy",
"./src/representers/EditRepresenter", "./src/representers/EditRepresenter",
"./src/representers/EditToolbarRepresenter", "./src/representers/EditToolbarRepresenter",
"text!./res/templates/edit.html",
"text!./res/templates/library.html", "text!./res/templates/library.html",
"text!./res/templates/edit-object.html", "text!./res/templates/edit-object.html",
"text!./res/templates/edit-action-buttons.html", "text!./res/templates/edit-action-buttons.html",
@ -44,10 +44,10 @@ define([
"text!./res/templates/topbar-edit.html", "text!./res/templates/topbar-edit.html",
'legacyRegistry' 'legacyRegistry'
], function ( ], function (
EditController,
EditActionController, EditActionController,
EditPanesController, EditPanesController,
ElementsController, ElementsController,
EditObjectController,
MCTBeforeUnload, MCTBeforeUnload,
LinkAction, LinkAction,
EditAction, EditAction,
@ -56,9 +56,9 @@ define([
SaveAction, SaveAction,
CancelAction, CancelAction,
EditActionPolicy, EditActionPolicy,
EditNavigationPolicy,
EditRepresenter, EditRepresenter,
EditToolbarRepresenter, EditToolbarRepresenter,
editTemplate,
libraryTemplate, libraryTemplate,
editObjectTemplate, editObjectTemplate,
editActionButtonsTemplate, editActionButtonsTemplate,
@ -70,22 +70,7 @@ define([
legacyRegistry.register("platform/commonUI/edit", { legacyRegistry.register("platform/commonUI/edit", {
"extensions": { "extensions": {
"routes": [
{
"when": "/edit",
"template": editTemplate
}
],
"controllers": [ "controllers": [
{
"key": "EditController",
"implementation": EditController,
"depends": [
"$scope",
"$q",
"navigationService"
]
},
{ {
"key": "EditActionController", "key": "EditActionController",
"implementation": EditActionController, "implementation": EditActionController,
@ -106,6 +91,15 @@ define([
"depends": [ "depends": [
"$scope" "$scope"
] ]
},
{
"key": "EditObjectController",
"implementation": EditObjectController,
"depends": [
"$scope",
"$location",
"policyService"
]
} }
], ],
"directives": [ "directives": [
@ -192,7 +186,13 @@ define([
{ {
"category": "action", "category": "action",
"implementation": EditActionPolicy "implementation": EditActionPolicy
},
{
"category": "navigation",
"message": "There are unsaved changes.",
"implementation": EditNavigationPolicy
} }
], ],
"templates": [ "templates": [
{ {
@ -206,6 +206,9 @@ define([
"template": editObjectTemplate, "template": editObjectTemplate,
"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

@ -124,7 +124,6 @@ define(
*/ */
EditorCapability.prototype.cancel = function () { EditorCapability.prototype.cancel = function () {
this.editableObject.getCapability("status").set("editing", false); this.editableObject.getCapability("status").set("editing", false);
//TODO: Reset the cache as well here.
this.cache.markClean(); this.cache.markClean();
return resolvePromise(undefined); return resolvePromise(undefined);
}; };

View File

@ -26,41 +26,45 @@
* @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, policyService) {
var self = this; this.scope = $scope;
this.policyService = policyService;
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 +72,18 @@ 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 && policyMessage;
navigatedObject.getCapability("editor"),
hasChanges = editorCapability && editorCapability.dirty(); this.policyService.allow("navigation", navigatedObject, undefined, function(message) {
policyMessage = message;
});
return policyMessage;
return hasChanges ?
"Unsaved changes will be lost if you leave this page." :
undefined;
}; };
return EditController; return EditObjectController;
} }
); );

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* 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*/
define(
[],
function () {
"use strict";
/**
* 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"),
statusCapability = navigatedObject &&
navigatedObject.getCapability("status");
return statusCapability && statusCapability.get('editing')
&& editorCapability && 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

@ -49,6 +49,7 @@ define(
var self = this; var self = this;
this.scope = scope; this.scope = scope;
this.listenHandle = undefined;
// Mutate and persist a new version of a domain object's model. // Mutate and persist a new version of a domain object's model.
function doPersist(model) { function doPersist(model) {
@ -100,10 +101,18 @@ define(
// Place the "commit" method in the scope // Place the "commit" method in the scope
scope.commit = commit; scope.commit = commit;
scope.setEditable = setEditable; scope.setEditable = setEditable;
// Clean up when the scope is destroyed
scope.$on("$destroy", function () {
self.destroy();
});
} }
// Handle a specific representation of a specific domain object // Handle a specific representation of a specific domain object
EditRepresenter.prototype.represent = function represent(representation, representedObject) { EditRepresenter.prototype.represent = function represent(representation, representedObject) {
var scope = this.scope,
self = this;
// Track the key, to know which view configuration to save to. // Track the key, to know which view configuration to save to.
this.key = (representation || {}).key; this.key = (representation || {}).key;
// Track the represented object // Track the represented object
@ -113,11 +122,32 @@ define(
// Ensure existing watches are released // Ensure existing watches are released
this.destroy(); 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.getCapability('status').get('editing')){
setEditing();
}
}; };
// Respond to the destruction of the current representation. // Respond to the destruction of the current representation.
EditRepresenter.prototype.destroy = function destroy() { EditRepresenter.prototype.destroy = function destroy() {
// Nothing to clean up return this.listenHandle && this.listenHandle();
}; };
return EditRepresenter; return EditRepresenter;

View File

@ -22,102 +22,110 @@
/*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,
mockPolicyService,
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 () {
mockPolicyService = jasmine.createSpyObj(
"policyService",
[
"allow"
]
);
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 mockPolicyService
); );
}); });
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']); errorMessage = "Unsaved changes";
// Normally, should be undefined // Normally, should be undefined
expect(controller.getUnloadWarning()).toBeUndefined(); expect(controller.getUnloadWarning()).toBeUndefined();
// Override the object's editor capability, make it look // Override the policy service to prevent navigation
// like there are unsaved changes. mockPolicyService.allow.andCallFake(function(category, object, context, callback){
obj.getCapability = jasmine.createSpy(); callback(errorMessage);
obj.getCapability.andReturn(mockEditor); });
mockEditor.dirty.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(errorMessage);
});
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

@ -33,8 +33,8 @@ define(
testRepresentation, testRepresentation,
mockDomainObject, mockDomainObject,
mockPersistence, mockPersistence,
mockCapabilities,
mockStatusCapability, mockStatusCapability,
mockCapabilities,
representer; representer;
function mockPromise(value) { function mockPromise(value) {
@ -48,7 +48,7 @@ define(
beforeEach(function () { beforeEach(function () {
mockQ = { when: mockPromise }; mockQ = { when: mockPromise };
mockLog = jasmine.createSpyObj("$log", ["info", "debug"]); mockLog = jasmine.createSpyObj("$log", ["info", "debug"]);
mockScope = jasmine.createSpyObj("$scope", ["$watch"]); mockScope = jasmine.createSpyObj("$scope", ["$watch", "$on"]);
testRepresentation = { key: "test" }; testRepresentation = { key: "test" };
mockDomainObject = jasmine.createSpyObj("domainObject", [ mockDomainObject = jasmine.createSpyObj("domainObject", [
"getId", "getId",
@ -60,7 +60,7 @@ define(
mockPersistence = mockPersistence =
jasmine.createSpyObj("persistence", ["persist"]); jasmine.createSpyObj("persistence", ["persist"]);
mockStatusCapability = mockStatusCapability =
jasmine.createSpyObj("status", ["get"]); jasmine.createSpyObj("statusCapability", ["get", "listen"]);
mockStatusCapability.get.andReturn(false); mockStatusCapability.get.andReturn(false);
mockCapabilities = { mockCapabilities = {
'persistence': mockPersistence, 'persistence': mockPersistence,
@ -82,6 +82,17 @@ define(
expect(mockScope.commit).toEqual(jasmine.any(Function)); expect(mockScope.commit).toEqual(jasmine.any(Function));
}); });
it("Sets edit view template on edit mode", function () {
mockStatusCapability.listen.mostRecentCall.args[0](['editing']);
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 and persists upon observed changes", function () { it("mutates and persists upon observed changes", function () {
mockScope.model = { someKey: "some value" }; mockScope.model = { someKey: "some value" };
mockScope.configuration = { someConfiguration: "something" }; mockScope.configuration = { someConfiguration: "something" };

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<span class="l-inspect"> <span class="l-inspect">
<div ng-controller="PaneController as modelPaneEdit"> <div ng-controller="PaneController">
<mct-split-pane class='abs contents split-layout' anchor='bottom'> <mct-split-pane class='abs contents split-layout' anchor='bottom'>
<div class="split-pane-component pane top"> <div class="split-pane-component pane top">
<div class="abs holder holder-inspector l-flex-col"> <div class="abs holder holder-inspector l-flex-col">
@ -31,8 +31,8 @@
ng-model="ngModel" ng-model="ngModel"
class="flex-elem grows vscroll l-flex-col"> class="flex-elem grows vscroll l-flex-col">
</mct-representation> </mct-representation>
</div><!--/ holder-inspector --> </div>
</div><!--/ split-pane-component --> </div>
<mct-splitter class="splitter-inspect-panel mobile-hide"></mct-splitter> <mct-splitter class="splitter-inspect-panel mobile-hide"></mct-splitter>
<div class="split-pane-component pane bottom"> <div class="split-pane-component pane bottom">
<div class="abs holder holder-elements l-flex-col"> <div class="abs holder holder-elements l-flex-col">
@ -45,5 +45,5 @@
</div> </div>
</div> </div>
</mct-split-pane> </mct-split-pane>
</div><!--/ PaneController --> </div>
</span> </span>

View File

@ -80,13 +80,6 @@ define(
}).length > 0; }).length > 0;
} }
function shouldCreateVirtualPanel(domainObject){
return domainObject.useCapability('view').filter(function (view){
return (view.key === 'plot' || view.key === 'scrolling')
&& domainObject.getModel().type !== 'telemetry.panel';
}).length > 0;
}
function dragOver(e) { function dragOver(e) {
//Refresh domain object on each dragOver to catch external //Refresh domain object on each dragOver to catch external
// updates to the model // updates to the model
@ -111,9 +104,7 @@ define(
key: 'compose', key: 'compose',
selectedObject: selectedObject selectedObject: selectedObject
})[0]; })[0];
//TODO: Fix this. Define an action for creating new if (action) {
// virtual panel
if (action || shouldCreateVirtualPanel(domainObject, selectedObject)) {
event.dataTransfer.dropEffect = 'move'; event.dataTransfer.dropEffect = 'move';
// Indicate that we will accept the drag // Indicate that we will accept the drag
@ -123,65 +114,24 @@ define(
} }
} }
function createVirtualPanel(base, selectedObject){
var typeKey = 'telemetry.panel',
type = typeService.getType(typeKey),
model = type.getInitialModel(),
newPanel,
composeAction;
model.type = typeKey;
newPanel = new EditableDomainObject(instantiate(model), $q);
if (!canCompose(newPanel, selectedObject)) {
return undefined;
}
[base.getId(), selectedObject.getId()].forEach(function(id){
newPanel.getCapability('composition').add(id);
});
newPanel.getCapability('location')
.setPrimaryLocation(base.getCapability('location')
.getContextualLocation());
newPanel.setOriginalObject(base);
return newPanel;
}
function drop(e) { function drop(e) {
var event = (e || {}).originalEvent || e, var event = (e || {}).originalEvent || e,
id = event.dataTransfer.getData(GestureConstants.MCT_DRAG_TYPE), id = event.dataTransfer.getData(GestureConstants.MCT_DRAG_TYPE),
domainObjectType = editableDomainObject.getModel().type, domainObjectType = editableDomainObject.getModel().type;
selectedObject = dndService.getData(
GestureConstants.MCT_EXTENDED_DRAG_TYPE
);
// Handle the drop; add the dropped identifier to the // Handle the drop; add the dropped identifier to the
// destination domain object's composition, and persist // destination domain object's composition, and persist
// the change. // the change.
if (id) { if (id) {
if (shouldCreateVirtualPanel(domainObject, selectedObject)){
editableDomainObject = createVirtualPanel(domainObject, selectedObject);
if (editableDomainObject) {
navigationService.setNavigation(editableDomainObject);
broadcastDrop(id, event);
editableDomainObject.getCapability('status').set('editing', true);
}
} 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);
}); });
} }
} }
// TODO: Alert user if drag and drop is not allowed
}
// We can only handle drops if we have access to actions... // We can only handle drops if we have access to actions...
if (actionCapability) { if (actionCapability) {