Merge pull request #8 from nasa/open1149

[Addressability] Use/populate URL
This commit is contained in:
Victor Woeltjen
2015-06-19 16:05:10 -07:00
8 changed files with 374 additions and 42 deletions

View File

@@ -2,19 +2,26 @@
"extensions": { "extensions": {
"routes": [ "routes": [
{ {
"when": "/browse", "when": "/browse/:ids*",
"templateUrl": "templates/browse.html" "templateUrl": "templates/browse.html",
"reloadOnSearch": false
}, },
{ {
"when": "", "when": "",
"templateUrl": "templates/browse.html" "templateUrl": "templates/browse.html",
"reloadOnSearch": false
} }
], ],
"controllers": [ "controllers": [
{ {
"key": "BrowseController", "key": "BrowseController",
"implementation": "BrowseController.js", "implementation": "BrowseController.js",
"depends": [ "$scope", "objectService", "navigationService" ] "depends": [ "$scope", "$route", "$location", "objectService", "navigationService" ]
},
{
"key": "BrowseObjectController",
"implementation": "BrowseObjectController.js",
"depends": [ "$scope", "$location", "$route" ]
}, },
{ {
"key": "CreateMenuController", "key": "CreateMenuController",
@@ -151,4 +158,4 @@
} }
] ]
} }
} }

View File

@@ -19,7 +19,7 @@
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.
--> -->
<span> <span ng-controller="BrowseObjectController">
<div class="object-browse-bar bar abs"> <div class="object-browse-bar bar abs">
<div class="items-select left abs"> <div class="items-select left abs">
<mct-representation key="'object-header'" mct-object="domainObject"> <mct-representation key="'object-header'" mct-object="domainObject">
@@ -44,4 +44,4 @@
mct-object="representation.selected.key && domainObject"> mct-object="representation.selected.key && domainObject">
</mct-representation> </mct-representation>
</div> </div>
</span> </span>

View File

@@ -29,7 +29,8 @@ define(
function () { function () {
"use strict"; "use strict";
var ROOT_OBJECT = "ROOT"; var ROOT_ID = "ROOT",
DEFAULT_PATH = "mine";
/** /**
* The BrowseController is used to populate the initial scope in Browse * The BrowseController is used to populate the initial scope in Browse
@@ -40,35 +41,98 @@ define(
* *
* @constructor * @constructor
*/ */
function BrowseController($scope, objectService, navigationService) { function BrowseController($scope, $route, $location, objectService, navigationService) {
var path = [ROOT_ID].concat(
($route.current.params.ids || DEFAULT_PATH).split("/")
);
function updateRoute(domainObject) {
var context = domainObject &&
domainObject.getCapability('context'),
objectPath = context ? context.getPath() : [],
ids = objectPath.map(function (domainObject) {
return domainObject.getId();
}),
priorRoute = $route.current,
// Act as if params HADN'T changed to avoid page reload
unlisten;
unlisten = $scope.$on('$locationChangeSuccess', function () {
$route.current = priorRoute;
unlisten();
});
$location.path("/browse/" + ids.slice(1).join("/"));
}
// 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) {
$scope.navigatedObject = domainObject; $scope.navigatedObject = domainObject;
$scope.treeModel.selectedObject = domainObject; $scope.treeModel.selectedObject = domainObject;
navigationService.setNavigation(domainObject); navigationService.setNavigation(domainObject);
updateRoute(domainObject);
}
function navigateTo(domainObject) {
// Check if an object has been navigated-to already...
// If not, or if an ID path has been explicitly set in the URL,
// navigate to the URL-specified object.
if (!navigationService.getNavigation() || $route.current.params.ids) {
// If not, pick a default as the last
// root-level component (usually "mine")
navigationService.setNavigation(domainObject);
$scope.navigatedObject = domainObject;
} else {
// Otherwise, just expose the currently navigated object.
$scope.navigatedObject = navigationService.getNavigation();
updateRoute($scope.navigatedObject);
}
}
function findObject(domainObjects, id) {
var i;
for (i = 0; i < domainObjects.length; i += 1) {
if (domainObjects[i].getId() === id) {
return domainObjects[i];
}
}
}
// Navigate to the domain object identified by path[index],
// which we expect to find in the composition of the passed
// domain object.
function doNavigate(domainObject, index) {
var composition = domainObject.useCapability("composition");
if (composition) {
composition.then(function (c) {
var nextObject = findObject(c, path[index]);
if (nextObject) {
if (index + 1 >= path.length) {
navigateTo(nextObject);
} else {
doNavigate(nextObject, index + 1);
}
} else {
// Couldn't find the next element of the path
// so navigate to the last path object we did find
navigateTo(domainObject);
}
});
} else {
// Similar to above case; this object has no composition,
// so navigate to it instead of subsequent path elements.
navigateTo(domainObject);
}
} }
// Load the root object, put it in the scope. // Load the root object, put it in the scope.
// Also, load its immediate children, and (possibly) // Also, load its immediate children, and (possibly)
// navigate to one of them, so that navigation state has // navigate to one of them, so that navigation state has
// a useful initial value. // a useful initial value.
objectService.getObjects([ROOT_OBJECT]).then(function (objects) { objectService.getObjects([path[0]]).then(function (objects) {
var composition = objects[ROOT_OBJECT].useCapability("composition"); $scope.domainObject = objects[path[0]];
$scope.domainObject = objects[ROOT_OBJECT]; doNavigate($scope.domainObject, 1);
if (composition) {
composition.then(function (c) {
// Check if an object has been navigated-to already...
if (!navigationService.getNavigation()) {
// If not, pick a default as the last
// root-level component (usually "mine")
navigationService.setNavigation(c[c.length - 1]);
} else {
// Otherwise, just expose it in the scope
$scope.navigatedObject = navigationService.getNavigation();
}
});
}
}); });
// Provide a model for the tree to modify // Provide a model for the tree to modify
@@ -91,4 +155,4 @@ define(
return BrowseController; return BrowseController;
} }
); );

View File

@@ -0,0 +1,69 @@
/*****************************************************************************
* 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,Promise*/
define(
[],
function () {
"use strict";
/**
* Controller for the `browse-object` representation of a domain
* object (the right-hand side of Browse mode.)
* @constructor
*/
function BrowseObjectController($scope, $location, $route) {
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);
}
}
function updateQueryParam(viewKey) {
var unlisten, priorRoute = $route.current;
if (viewKey) {
$location.search('view', viewKey);
unlisten = $scope.$on('$locationChangeSuccess', function () {
$route.current = priorRoute;
unlisten();
});
}
}
$scope.$watch('domainObject', setViewForDomainObject);
$scope.$watch('representation.selected.key', updateQueryParam);
}
return BrowseObjectController;
}
);

View File

@@ -31,10 +31,13 @@ define(
describe("The browse controller", function () { describe("The browse controller", function () {
var mockScope, var mockScope,
mockRoute,
mockLocation,
mockObjectService, mockObjectService,
mockNavigationService, mockNavigationService,
mockRootObject, mockRootObject,
mockDomainObject, mockDomainObject,
mockNextObject,
controller; controller;
function mockPromise(value) { function mockPromise(value) {
@@ -50,6 +53,11 @@ define(
"$scope", "$scope",
[ "$on", "$watch" ] [ "$on", "$watch" ]
); );
mockRoute = { current: { params: {} } };
mockLocation = jasmine.createSpyObj(
"$location",
[ "path" ]
);
mockObjectService = jasmine.createSpyObj( mockObjectService = jasmine.createSpyObj(
"objectService", "objectService",
[ "getObjects" ] [ "getObjects" ]
@@ -71,25 +79,38 @@ define(
"domainObject", "domainObject",
[ "getId", "getCapability", "getModel", "useCapability" ] [ "getId", "getCapability", "getModel", "useCapability" ]
); );
mockNextObject = jasmine.createSpyObj(
"nextObject",
[ "getId", "getCapability", "getModel", "useCapability" ]
);
mockObjectService.getObjects.andReturn(mockPromise({ mockObjectService.getObjects.andReturn(mockPromise({
ROOT: mockRootObject ROOT: mockRootObject
})); }));
mockRootObject.useCapability.andReturn(mockPromise([
mockDomainObject
]));
mockDomainObject.useCapability.andReturn(mockPromise([
mockNextObject
]));
mockNextObject.useCapability.andReturn(undefined);
mockNextObject.getId.andReturn("next");
mockDomainObject.getId.andReturn("mine");
controller = new BrowseController( controller = new BrowseController(
mockScope, mockScope,
mockRoute,
mockLocation,
mockObjectService, mockObjectService,
mockNavigationService mockNavigationService
); );
}); });
it("uses composition to set the navigated object, if there is none", function () { it("uses composition to set the navigated object, if there is none", function () {
mockRootObject.useCapability.andReturn(mockPromise([
mockDomainObject
]));
controller = new BrowseController( controller = new BrowseController(
mockScope, mockScope,
mockRoute,
mockLocation,
mockObjectService, mockObjectService,
mockNavigationService mockNavigationService
); );
@@ -98,12 +119,11 @@ define(
}); });
it("does not try to override navigation", function () { it("does not try to override navigation", function () {
// This behavior is needed if object navigation has been
// determined by query string parameters
mockRootObject.useCapability.andReturn(mockPromise([null]));
mockNavigationService.getNavigation.andReturn(mockDomainObject); mockNavigationService.getNavigation.andReturn(mockDomainObject);
controller = new BrowseController( controller = new BrowseController(
mockScope, mockScope,
mockRoute,
mockLocation,
mockObjectService, mockObjectService,
mockNavigationService mockNavigationService
); );
@@ -130,6 +150,76 @@ define(
); );
}); });
it("uses route parameters to choose initially-navigated object", function () {
mockRoute.current.params.ids = "mine/next";
controller = new BrowseController(
mockScope,
mockRoute,
mockLocation,
mockObjectService,
mockNavigationService
);
expect(mockScope.navigatedObject).toBe(mockNextObject);
expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockNextObject);
});
it("handles invalid IDs by going as far as possible", function () {
// Idea here is that if we get a bad path of IDs,
// browse controller should traverse down it until
// it hits an invalid ID.
mockRoute.current.params.ids = "mine/junk";
controller = new BrowseController(
mockScope,
mockRoute,
mockLocation,
mockObjectService,
mockNavigationService
);
expect(mockScope.navigatedObject).toBe(mockDomainObject);
expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockDomainObject);
});
it("handles compositionless objects by going as far as possible", function () {
// Idea here is that if we get a path which passes
// through an object without a composition, browse controller
// should stop at it since remaining IDs cannot be loaded.
mockRoute.current.params.ids = "mine/next/junk";
controller = new BrowseController(
mockScope,
mockRoute,
mockLocation,
mockObjectService,
mockNavigationService
);
expect(mockScope.navigatedObject).toBe(mockNextObject);
expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockNextObject);
});
it("updates the displayed route to reflect current navigation", function () {
var mockContext = jasmine.createSpyObj('context', ['getPath']),
mockUnlisten = jasmine.createSpy('unlisten');
mockContext.getPath.andReturn(
[mockRootObject, mockDomainObject, mockNextObject]
);
mockNextObject.getCapability.andCallFake(function (c) {
return c === 'context' && mockContext;
});
mockScope.$on.andReturn(mockUnlisten);
// Provide a navigation change
mockNavigationService.addListener.mostRecentCall.args[0](
mockNextObject
);
expect(mockLocation.path).toHaveBeenCalledWith("/browse/mine/next");
// Exercise the Angular workaround
mockScope.$on.mostRecentCall.args[1]();
expect(mockUnlisten).toHaveBeenCalled();
});
}); });
} }
); );

View File

@@ -0,0 +1,99 @@
/*****************************************************************************
* 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,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/BrowseObjectController"],
function (BrowseObjectController) {
"use strict";
describe("The browse object controller", function () {
var mockScope,
mockLocation,
mockRoute,
mockUnlisten,
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 () {
mockScope = jasmine.createSpyObj(
"$scope",
[ "$on", "$watch" ]
);
mockRoute = { current: { params: {} } };
mockLocation = jasmine.createSpyObj(
"$location",
[ "path", "search" ]
);
mockUnlisten = jasmine.createSpy("unlisten");
mockScope.$on.andReturn(mockUnlisten);
controller = new BrowseObjectController(
mockScope,
mockLocation,
mockRoute
);
});
it("updates query parameters when selected view changes", function () {
fireWatch("representation.selected.key", "xyz");
expect(mockLocation.search).toHaveBeenCalledWith('view', "xyz");
// Exercise the Angular workaround
mockScope.$on.mostRecentCall.args[1]();
expect(mockUnlisten).toHaveBeenCalled();
});
it("sets the active view from query parameters", function () {
var mockDomainObject = jasmine.createSpyObj(
"domainObject",
['getId', 'getModel', 'getCapability', 'useCapability']
),
testViews = [
{ key: 'abc' },
{ key: 'def', someKey: 'some value' },
{ key: 'xyz' }
];
mockDomainObject.useCapability.andCallFake(function (c) {
return (c === 'view') && testViews;
});
mockLocation.search.andReturn({ view: 'def' });
fireWatch('domainObject', mockDomainObject);
expect(mockScope.representation.selected)
.toEqual(testViews[1]);
});
});
}
);

View File

@@ -1,5 +1,6 @@
[ [
"BrowseController", "BrowseController",
"BrowseObjectController",
"creation/CreateAction", "creation/CreateAction",
"creation/CreateActionProvider", "creation/CreateActionProvider",
"creation/CreateMenuController", "creation/CreateMenuController",
@@ -10,4 +11,4 @@
"navigation/NavigationService", "navigation/NavigationService",
"windowing/FullscreenAction", "windowing/FullscreenAction",
"windowing/WindowTitler" "windowing/WindowTitler"
] ]

View File

@@ -53,12 +53,14 @@ define(
// Get list of views, read from capability // Get list of views, read from capability
function updateOptions(views) { function updateOptions(views) {
$timeout(function () { if (Array.isArray(views)) {
$scope.ngModel.selected = findMatchingOption( $timeout(function () {
views || [], $scope.ngModel.selected = findMatchingOption(
($scope.ngModel || {}).selected views,
); ($scope.ngModel || {}).selected
}, 0); );
}, 0);
}
} }
// Update view options when the in-scope results of using the // Update view options when the in-scope results of using the
@@ -68,4 +70,4 @@ define(
return ViewSwitcherController; return ViewSwitcherController;
} }
); );