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

View File

@ -47,11 +47,6 @@
<div ng-if="isEditable" 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"
structure="toolbar.structure"
ng-model="toolbar.state"
class="flex-elem grows">
</mct-toolbar>
<mct-representation key="'edit-action-buttons'"
mct-object="domainObject"
class='flex-elem conclude-editing'>

View File

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

View File

@ -19,14 +19,15 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div content="jquery-wrapper"
class="abs holder-all edit-mode"
ng-controller="EditController as editMode"
mct-before-unload="editMode.getUnloadWarning()">
<!--
A representation that allows the 'View' region of an object view to change
dynamically (eg. between browse and edit modes). Values correspond to a
representation key, and currently defaults to 'browse-object'.
<mct-representation key="'edit-object'" mct-object="editMode.navigatedObject()">
</mct-representation>
<mct-include key="'bottombar'"></mct-include>
</div>
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>

View File

@ -27,14 +27,12 @@
*/
define(
[
'../../../representation/src/gestures/GestureConstants',
'../../edit/src/objects/EditableDomainObject'
'../../../representation/src/gestures/GestureConstants'
],
function (GestureConstants, EditableDomainObject) {
function (GestureConstants) {
"use strict";
var ROOT_ID = "ROOT",
CONFIRM_MSG = "Unsaved changes will be lost if you leave this page.";
var ROOT_ID = "ROOT";
/**
* The BrowseController is used to populate the initial scope in Browse
@ -47,26 +45,20 @@ define(
* @constructor
*/
function BrowseController(
$scope,
$route,
$location,
$q,
objectService,
navigationService,
urlService,
$scope,
$route,
$location,
$window,
objectService,
navigationService,
urlService,
policyService,
defaultPath
) {
var path = [ROOT_ID].concat(
($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) {
var priorRoute = $route.current,
// Act as if params HADN'T changed to avoid page reload
@ -83,31 +75,35 @@ define(
// urlService.urlForLocation used to adjust current
// path to new, addressed, path based on
// domainObject
$location.path(urlService.urlForLocation("browse",
domainObject.hasCapability('editor') ?
domainObject.getOriginalObject() : domainObject));
$location.path(urlService.urlForLocation("browse", domainObject));
}
// Callback for updating the in-scope reference to the object
// that is currently navigated-to.
function setNavigation(domainObject) {
var navigationAllowed = true;
if (domainObject === $scope.navigatedObject){
//do nothing;
return;
}
if (isDirty() && !confirm(CONFIRM_MSG)) {
$scope.treeModel.selectedObject = $scope.navigatedObject;
navigationService.setNavigation($scope.navigatedObject);
} else {
if ($scope.navigatedObject && $scope.navigatedObject.hasCapability("editor")){
$scope.navigatedObject.getCapability("editor").cancel();
}
policyService.allow("navigation", $scope.navigatedObject, domainObject, function(message){
navigationAllowed = $window.confirm(message + "\r\n\r\n" +
" Are you sure you want to continue?");
});
if (navigationAllowed) {
$scope.navigatedObject = domainObject;
$scope.treeModel.selectedObject = domainObject;
navigationService.setNavigation(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,18 +180,13 @@ define(
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.
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);
// Clean up when the scope is destroyed
$scope.$on("$destroy", function () {
navigationService.removeListener(setNavigation);

View File

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

View File

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

View File

@ -29,8 +29,7 @@ define(
function (BrowseController) {
"use strict";
//TODO: Disabled for NEM Beta
xdescribe("The browse controller", function () {
describe("The browse controller", function () {
var mockScope,
mockRoute,
mockLocation,
@ -40,6 +39,8 @@ define(
mockUrlService,
mockDomainObject,
mockNextObject,
mockWindow,
mockPolicyService,
testDefaultRoot,
controller;
@ -56,14 +57,25 @@ define(
mockScope,
mockRoute,
mockLocation,
mockWindow,
mockObjectService,
mockNavigationService,
mockUrlService,
mockPolicyService,
testDefaultRoot
);
}
beforeEach(function () {
mockWindow = jasmine.createSpyObj('$window', [
"confirm"
]);
mockWindow.confirm.andReturn(true);
mockPolicyService = jasmine.createSpyObj('policyService', [
'allow'
]);
testDefaultRoot = "some-root-level-domain-object";
mockScope = jasmine.createSpyObj(
@ -214,7 +226,10 @@ define(
// prior to setting $route.current
mockLocation.path.andReturn("/browse/");
mockNavigationService.setNavigation.andReturn(true);
// Exercise the Angular workaround
mockNavigationService.addListener.mostRecentCall.args[0]();
mockScope.$on.mostRecentCall.args[1]();
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

@ -86,4 +86,4 @@ define(
});
}
);
);

View File

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

View File

@ -19,50 +19,51 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<mct-representation key="'topbar-edit'"
mct-object="domainObject"
ng-model="representation">
</mct-representation>
<div class="holder edit-area abs">
<mct-split-pane class='contents abs' anchor='right'>
<div class='split-pane-component pane left edit-main'>
<mct-toolbar name="mctToolbar"
structure="toolbar.structure"
ng-model="toolbar.state">
</mct-toolbar>
<mct-representation key="representation.selected.key"
toolbar="toolbar"
mct-object="representation.selected.key && domainObject"
class="holder abs object-holder work-area">
<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>
<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 class="btn-bar right l-flex-row flex-elem flex-justify-end flex-fixed">
<mct-representation key="'switcher'"
mct-object="domainObject"
ng-model="representation">
</mct-representation>
<!-- Temporarily, on mobile, the action buttons are hidden-->
<mct-representation key="'action-group'"
mct-object="domainObject"
parameters="{ category: 'view-control' }"
class="mobile-hide">
</mct-representation>
</div>
</mct-split-pane>
</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"
structure="toolbar.structure"
ng-model="toolbar.state"
class="flex-elem grows">
</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-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll"
toolbar="toolbar">
</mct-representation>
</div><!--/ l-object-wrapper-inner -->
</div>
</div>

View File

@ -72,13 +72,26 @@ define(
* Enter edit mode.
*/
EditAction.prototype.perform = function () {
var editableObject;
var self = this;
if (!this.domainObject.hasCapability("editor")) {
editableObject = new EditableDomainObject(this.domainObject, this.$q);
editableObject.getCapability('status').set('editing', true);
this.navigationService.setNavigation(editableObject);
//TODO: This is only necessary because the drop gesture is
// wrapping the object itself, need to refactor this later.
// 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 () {
this.editableObject.getCapability("status").set("editing", false);
//TODO: Reset the cache as well here.
this.cache.markClean();
return resolvePromise(undefined);
};

View File

@ -26,41 +26,45 @@
* @namespace platform/commonUI/edit
*/
define(
["../objects/EditableDomainObject"],
function (EditableDomainObject) {
[],
function () {
"use strict";
/**
* Controller which is responsible for populating the scope for
* Edit mode; introduces an editable version of the currently
* navigated domain object into the scope.
* Edit mode
* @memberof platform/commonUI/edit
* @constructor
*/
function EditController($scope, $q, navigationService) {
var self = this;
function EditObjectController($scope, $location, policyService) {
this.scope = $scope;
this.policyService = policyService;
function setNavigation(domainObject) {
// Wrap the domain object such that all mutation is
// confined to edit mode (until Save)
self.navigatedDomainObject =
domainObject && new EditableDomainObject(domainObject, $q);
var navigatedObject;
function setViewForDomainObject(domainObject) {
var locationViewKey = $location.search().view;
function selectViewIfMatching(view) {
if (view.key === locationViewKey) {
$scope.representation = $scope.representation || {};
$scope.representation.selected = view;
}
}
if (locationViewKey) {
((domainObject && domainObject.useCapability('view')) || [])
.forEach(selectViewIfMatching);
}
navigatedObject = domainObject;
}
setNavigation(navigationService.getNavigation());
navigationService.addListener(setNavigation);
$scope.$on("$destroy", function () {
navigationService.removeListener(setNavigation);
});
}
$scope.$watch('domainObject', setViewForDomainObject);
/**
* Get the domain object which is navigated-to.
* @returns {DomainObject} the domain object that is navigated-to
*/
EditController.prototype.navigatedObject = function () {
return this.navigatedDomainObject;
};
$scope.doAction = function (action){
return $scope[action] && $scope[action]();
};
}
/**
* 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
* there are no unsaved changes
*/
EditController.prototype.getUnloadWarning = function () {
var navigatedObject = this.navigatedDomainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor"),
hasChanges = editorCapability && editorCapability.dirty();
EditObjectController.prototype.getUnloadWarning = function () {
var navigatedObject = this.scope.domainObject,
policyMessage;
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;
this.scope = scope;
this.listenHandle = undefined;
// Mutate and persist a new version of a domain object's model.
function doPersist(model) {
@ -100,10 +101,18 @@ define(
// Place the "commit" method in the scope
scope.commit = commit;
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
EditRepresenter.prototype.represent = function represent(representation, representedObject) {
var scope = this.scope,
self = this;
// Track the key, to know which view configuration to save to.
this.key = (representation || {}).key;
// Track the represented object
@ -113,11 +122,32 @@ define(
// Ensure existing watches are released
this.destroy();
function setEditing(){
scope.viewObjectTemplate = 'edit-object';
}
/**
* Listen for changes in object state. If the object becomes
* editable then change the view and inspector regions
* object representation accordingly
*/
this.listenHandle = this.domainObject.getCapability('status').listen(function(statuses){
if (statuses.indexOf('editing')!=-1){
setEditing();
} else {
delete scope.viewObjectTemplate;
}
});
if (representedObject.getCapability('status').get('editing')){
setEditing();
}
};
// Respond to the destruction of the current representation.
EditRepresenter.prototype.destroy = function destroy() {
// Nothing to clean up
return this.listenHandle && this.listenHandle();
};
return EditRepresenter;

View File

@ -22,102 +22,110 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../../src/controllers/EditController"],
function (EditController) {
["../../src/controllers/EditObjectController"],
function (EditObjectController) {
"use strict";
describe("The Edit mode controller", function () {
var mockScope,
mockQ,
mockNavigationService,
mockObject,
mockType,
mockLocation,
mockStatusCapability,
mockCapabilities,
mockPolicyService,
controller;
// Utility function; look for a $watch on scope and fire it
function fireWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockPolicyService = jasmine.createSpyObj(
"policyService",
[
"allow"
]
);
mockScope = jasmine.createSpyObj(
"$scope",
[ "$on" ]
);
mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[ "getNavigation", "addListener", "removeListener" ]
[ "$on", "$watch" ]
);
mockObject = jasmine.createSpyObj(
"domainObject",
[ "getId", "getModel", "getCapability", "hasCapability" ]
[ "getId", "getModel", "getCapability", "hasCapability", "useCapability" ]
);
mockType = jasmine.createSpyObj(
"type",
[ "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.getModel.andReturn({ name: "Test object" });
mockObject.getCapability.andCallFake(function (key) {
return key === 'type' && mockType;
return mockCapabilities[key];
});
mockType.hasFeature.andReturn(true);
controller = new EditController(
mockScope.domainObject = mockObject;
controller = new EditObjectController(
mockScope,
mockQ,
mockNavigationService
mockLocation,
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 () {
var obj = controller.navigatedObject(),
mockEditor = jasmine.createSpyObj('editor', ['dirty']);
var obj = mockObject,
errorMessage = "Unsaved changes";
// 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);
// Override the policy service to prevent navigation
mockPolicyService.allow.andCallFake(function(category, object, context, callback){
callback(errorMessage);
});
// Should have some warning message here now
expect(controller.getUnloadWarning()).toEqual(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,
mockDomainObject,
mockPersistence,
mockCapabilities,
mockStatusCapability,
mockCapabilities,
representer;
function mockPromise(value) {
@ -48,7 +48,7 @@ define(
beforeEach(function () {
mockQ = { when: mockPromise };
mockLog = jasmine.createSpyObj("$log", ["info", "debug"]);
mockScope = jasmine.createSpyObj("$scope", ["$watch"]);
mockScope = jasmine.createSpyObj("$scope", ["$watch", "$on"]);
testRepresentation = { key: "test" };
mockDomainObject = jasmine.createSpyObj("domainObject", [
"getId",
@ -60,7 +60,7 @@ define(
mockPersistence =
jasmine.createSpyObj("persistence", ["persist"]);
mockStatusCapability =
jasmine.createSpyObj("status", ["get"]);
jasmine.createSpyObj("statusCapability", ["get", "listen"]);
mockStatusCapability.get.andReturn(false);
mockCapabilities = {
'persistence': mockPersistence,
@ -82,6 +82,17 @@ define(
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 () {
mockScope.model = { someKey: "some value" };
mockScope.configuration = { someConfiguration: "something" };
@ -112,4 +123,4 @@ define(
});
}
);
);

View File

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

View File

@ -80,13 +80,6 @@ define(
}).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) {
//Refresh domain object on each dragOver to catch external
// updates to the model
@ -111,9 +104,7 @@ define(
key: 'compose',
selectedObject: selectedObject
})[0];
//TODO: Fix this. Define an action for creating new
// virtual panel
if (action || shouldCreateVirtualPanel(domainObject, selectedObject)) {
if (action) {
event.dataTransfer.dropEffect = 'move';
// Indicate that we will accept the drag
@ -123,64 +114,23 @@ 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) {
var event = (e || {}).originalEvent || e,
id = event.dataTransfer.getData(GestureConstants.MCT_DRAG_TYPE),
domainObjectType = editableDomainObject.getModel().type,
selectedObject = dndService.getData(
GestureConstants.MCT_EXTENDED_DRAG_TYPE
);
domainObjectType = editableDomainObject.getModel().type;
// Handle the drop; add the dropped identifier to the
// destination domain object's composition, and persist
// the change.
if (id) {
if (shouldCreateVirtualPanel(domainObject, selectedObject)){
editableDomainObject = createVirtualPanel(domainObject, selectedObject);
if (editableDomainObject) {
navigationService.setNavigation(editableDomainObject);
broadcastDrop(id, event);
editableDomainObject.getCapability('status').set('editing', true);
$q.when(action && action.perform()).then(function (result) {
//Don't go into edit mode for folders
if (domainObjectType!=='folder') {
editableDomainObject.getCapability('action').perform('edit');
}
} else {
$q.when(action && action.perform()).then(function (result) {
//Don't go into edit mode for folders
if (domainObjectType!=='folder') {
navigationService.setNavigation(editableDomainObject);
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...