Compare commits
	
		
			122 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | e8e9598721 | ||
|   | ce87ad2564 | ||
|   | 2a2f6e8142 | ||
|   | 3e5057c285 | ||
|   | 5485950130 | ||
|   | c8f4568bd0 | ||
|   | cefb40856b | ||
|   | ee7c450e11 | ||
|   | d741e0f23c | ||
|   | 8080490e5c | ||
|   | a3443d8077 | ||
|   | 53e8e7c688 | ||
|   | dea94e4e68 | ||
|   | d40c7f1821 | ||
|   | c7e7e0c302 | ||
|   | 8ac6ac97f0 | ||
|   | 1afa4ab329 | ||
|   | c2517c1670 | ||
|   | 717ceff02c | ||
|   | 6878c59f45 | ||
|   | c00b053aa7 | ||
|   | 480e12c3ab | ||
|   | 50bd233b0a | ||
|   | 79406cf1ed | ||
|   | 2d5824c4ab | ||
|   | 3480809129 | ||
|   | 11bc48c7e0 | ||
|   | d759401b69 | ||
|   | 5a2e5ac48f | ||
|   | 1144f818cf | ||
|   | 0aebecfbb0 | ||
|   | 531d40993a | ||
|   | 4c097faf88 | ||
|   | 5152e64895 | ||
|   | 0263237b2c | ||
|   | acd0fae040 | ||
|   | 29dd51439d | ||
|   | a1b2175801 | ||
|   | b0f06a2195 | ||
|   | f9c93ca022 | ||
|   | 8e0858bb24 | ||
|   | ee0fa0451a | ||
|   | e86e955682 | ||
|   | 9913fb48f5 | ||
|   | 71c362f016 | ||
|   | fa6e8fd5f9 | ||
|   | 23b64951f3 | ||
|   | 99590d18f7 | ||
|   | 86b31bc040 | ||
|   | d52bfed1df | ||
|   | 808ccd0376 | ||
|   | 44d6456de1 | ||
|   | a394b95259 | ||
|   | beeefe517a | ||
|   | d02f4041b2 | ||
|   | 9fd75ff91e | ||
|   | 026ece3956 | ||
|   | 214a843dba | ||
|   | 1d6880c283 | ||
|   | 8ddad9bf4c | ||
|   | f167022eea | ||
|   | 76edba1014 | ||
|   | 7904989a23 | ||
|   | ea676b4368 | ||
|   | cc2b102256 | ||
|   | b1266abf01 | ||
|   | cc7d0477e8 | ||
|   | 5a2d1a746d | ||
|   | 4f0e3fdf85 | ||
|   | be9f56107c | ||
|   | 787f3815df | ||
|   | 35d7d9b380 | ||
|   | 8b9c51f303 | ||
|   | 661b3d5889 | ||
|   | 01c85cb58d | ||
|   | 0218f42e2b | ||
|   | d8d0f22889 | ||
|   | d8a097a95a | ||
|   | dc577d4c24 | ||
|   | 8f9308de01 | ||
|   | 8f7a5e113b | ||
|   | 9820f9d9c5 | ||
|   | 56ff98cce7 | ||
|   | dade6b2254 | ||
|   | e9cac6eff3 | ||
|   | 7fc2fcfa07 | ||
|   | 5689279954 | ||
|   | 165e158f37 | ||
|   | f301741852 | ||
|   | eda1f23df9 | ||
|   | d15d27af73 | ||
|   | 438511c5f7 | ||
|   | 3eb960cf5a | ||
|   | 699f6ba458 | ||
|   | f21f22d95c | ||
|   | b520d08818 | ||
|   | f9fd97230f | ||
|   | 536e2290b8 | ||
|   | 73b922facf | ||
|   | ba0d9a186b | ||
|   | 80f5cb756d | ||
|   | d7f566088f | ||
|   | a3bcaea7f9 | ||
|   | 23c71b7218 | ||
|   | 463f7ccf65 | ||
|   | 87fe407739 | ||
|   | bb4f1ce7cd | ||
|   | 0cc2ba7595 | ||
|   | 8162429106 | ||
|   | ed519d89d7 | ||
|   | 0e4f6185b8 | ||
|   | 1ced47fc2c | ||
|   | 677b65d124 | ||
|   | 31d31d7819 | ||
|   | d32ef4bc3d | ||
|   | b4faf8991d | ||
|   | 0564759481 | ||
|   | 24e391edf7 | ||
|   | cf295105d4 | ||
|   | f16a107105 | ||
|   | f683ca44a2 | ||
|   | 546cde56a8 | 
| @@ -1,4 +1,4 @@ | ||||
| # Open MCT | ||||
| # Open MCT [](http://www.apache.org/licenses/LICENSE-2.0) | ||||
|  | ||||
| Open MCT is a web-based platform for mission operations user interface | ||||
| software. | ||||
| @@ -7,7 +7,7 @@ software. | ||||
|  | ||||
| A bundle is a group of software components (including source code, declared | ||||
| as AMD modules, as well as resources such as images and HTML templates) | ||||
| that are intended to be added or removed as a single unit. A plug-in for | ||||
| that is intended to be added or removed as a single unit. A plug-in for | ||||
| Open MCT will be expressed as a bundle; platform components are also | ||||
| expressed as bundles. | ||||
|  | ||||
| @@ -133,6 +133,6 @@ documentation, may presume an understanding of these terms. | ||||
|   it, and it is thereafter considered the _navigated_ object (until the | ||||
|   user makes another such choice.) | ||||
| * _space_: A name used to identify a persistence store. Interactions with | ||||
|   persistence with generally involve a `space` parameter in some form, to | ||||
|   persistence will generally involve a `space` parameter in some form, to | ||||
|   distinguish multiple persistence stores from one another (for cases | ||||
|   where there are multiple valid persistence locations available.) | ||||
|   | ||||
							
								
								
									
										8
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								app.js
									
									
									
									
									
								
							| @@ -75,6 +75,8 @@ | ||||
|     // Expose everything else as static files | ||||
|     app.use(express['static'](options.directory)); | ||||
|  | ||||
|     // Finally, open the HTTP server | ||||
|     app.listen(options.port); | ||||
| }()); | ||||
|     // Finally, open the HTTP server and log the instance to the console | ||||
|     app.listen(options.port, function() { | ||||
|         console.log('Open MCT application running at localhost:' + options.port) | ||||
|     }); | ||||
| }()); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "0.10.2-SNAPSHOT", | ||||
|   "version": "0.10.3", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": { | ||||
|     "express": "^4.13.1", | ||||
|   | ||||
| @@ -27,6 +27,7 @@ define([ | ||||
|     "./src/MenuArrowController", | ||||
|     "./src/navigation/NavigationService", | ||||
|     "./src/navigation/NavigateAction", | ||||
|     "./src/navigation/OrphanNavigationHandler", | ||||
|     "./src/windowing/NewTabAction", | ||||
|     "./src/windowing/FullscreenAction", | ||||
|     "./src/windowing/WindowTitler", | ||||
| @@ -47,6 +48,7 @@ define([ | ||||
|     MenuArrowController, | ||||
|     NavigationService, | ||||
|     NavigateAction, | ||||
|     OrphanNavigationHandler, | ||||
|     NewTabAction, | ||||
|     FullscreenAction, | ||||
|     WindowTitler, | ||||
| @@ -91,11 +93,9 @@ define([ | ||||
|                         "$scope", | ||||
|                         "$route", | ||||
|                         "$location", | ||||
|                         "$window", | ||||
|                         "objectService", | ||||
|                         "navigationService", | ||||
|                         "urlService", | ||||
|                         "policyService", | ||||
|                         "DEFAULT_PATH" | ||||
|                     ] | ||||
|                 }, | ||||
| @@ -199,7 +199,9 @@ define([ | ||||
|                     "implementation": NavigateAction, | ||||
|                     "depends": [ | ||||
|                         "navigationService", | ||||
|                         "$q" | ||||
|                         "$q", | ||||
|                         "policyService", | ||||
|                         "$window" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
| @@ -253,6 +255,14 @@ define([ | ||||
|                         "$rootScope", | ||||
|                         "$document" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "implementation": OrphanNavigationHandler, | ||||
|                     "depends": [ | ||||
|                         "throttle", | ||||
|                         "topic", | ||||
|                         "navigationService" | ||||
|                     ] | ||||
|                 } | ||||
|             ], | ||||
|             "licenses": [ | ||||
|   | ||||
| @@ -44,11 +44,9 @@ define( | ||||
|             $scope, | ||||
|             $route, | ||||
|             $location, | ||||
|             $window, | ||||
|             objectService, | ||||
|             navigationService, | ||||
|             urlService, | ||||
|             policyService, | ||||
|             defaultPath | ||||
|         ) { | ||||
|             var path = [ROOT_ID].concat( | ||||
| @@ -75,25 +73,10 @@ define( | ||||
|  | ||||
|             } | ||||
|  | ||||
|             // 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; | ||||
|                 } | ||||
|  | ||||
|                 policyService.allow("navigation", $scope.navigatedObject, domainObject, function (message) { | ||||
|                     navigationAllowed = $window.confirm(message + "\r\n\r\n" + | ||||
|                         " Are you sure you want to continue?"); | ||||
|                 }); | ||||
|  | ||||
|             function setScopeObjects(domainObject, navigationAllowed) { | ||||
|                 if (navigationAllowed) { | ||||
|                     $scope.navigatedObject = domainObject; | ||||
|                     $scope.treeModel.selectedObject = domainObject; | ||||
|                     navigationService.setNavigation(domainObject); | ||||
|                     updateRoute(domainObject); | ||||
|                 } else { | ||||
|                     //If navigation was unsuccessful (ie. blocked), reset | ||||
| @@ -103,6 +86,20 @@ define( | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Callback for updating the in-scope reference to the object | ||||
|             // that is currently navigated-to. | ||||
|             function setNavigation(domainObject) { | ||||
|                 if (domainObject === $scope.navigatedObject) { | ||||
|                     //do nothing; | ||||
|                     return; | ||||
|                 } | ||||
|                 if (domainObject) { | ||||
|                     domainObject.getCapability("action").perform("navigate").then(setScopeObjects.bind(undefined, domainObject)); | ||||
|                 } else { | ||||
|                     setScopeObjects(domainObject, true); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             function navigateTo(domainObject) { | ||||
|  | ||||
|                 // Check if an object has been navigated-to already... | ||||
|   | ||||
| @@ -33,10 +33,12 @@ define( | ||||
|          * @constructor | ||||
|          * @implements {Action} | ||||
|          */ | ||||
|         function NavigateAction(navigationService, $q, context) { | ||||
|         function NavigateAction(navigationService, $q, policyService, $window, context) { | ||||
|             this.domainObject = context.domainObject; | ||||
|             this.$q = $q; | ||||
|             this.navigationService = navigationService; | ||||
|             this.policyService = policyService; | ||||
|             this.$window = $window; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
| @@ -45,9 +47,20 @@ define( | ||||
|          *          navigation has been updated | ||||
|          */ | ||||
|         NavigateAction.prototype.perform = function () { | ||||
|             var self = this, | ||||
|                 navigationAllowed = true; | ||||
|  | ||||
|             function allow() { | ||||
|                 self.policyService.allow("navigation", self.navigationService.getNavigation(), self.domainObject, function (message) { | ||||
|                     navigationAllowed = self.$window.confirm(message + "\r\n\r\n" + | ||||
|                         " Are you sure you want to continue?"); | ||||
|                 }); | ||||
|                 return navigationAllowed; | ||||
|             } | ||||
|  | ||||
|             // Set navigation, and wrap like a promise | ||||
|             return this.$q.when( | ||||
|                 this.navigationService.setNavigation(this.domainObject) | ||||
|                 allow() && this.navigationService.setNavigation(this.domainObject) | ||||
|             ); | ||||
|         }; | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,75 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([], function () { | ||||
|  | ||||
|     /** | ||||
|      * Navigates away from orphan objects whenever they are detected. | ||||
|      * | ||||
|      * An orphan object is an object whose apparent parent does not | ||||
|      * actually contain it. This may occur in certain circumstances, such | ||||
|      * as when persistence succeeds for a newly-created object but fails | ||||
|      * for its parent. | ||||
|      * | ||||
|      * @param throttle the `throttle` service | ||||
|      * @param topic the `topic` service | ||||
|      * @param navigationService the `navigationService` | ||||
|      * @constructor | ||||
|      */ | ||||
|     function OrphanNavigationHandler(throttle, topic, navigationService) { | ||||
|         var throttledCheckNavigation; | ||||
|  | ||||
|         function getParent(domainObject) { | ||||
|             var context = domainObject.getCapability('context'); | ||||
|             return context.getParent(); | ||||
|         } | ||||
|  | ||||
|         function isOrphan(domainObject) { | ||||
|             var parent = getParent(domainObject), | ||||
|                 composition = parent.getModel().composition, | ||||
|                 id = domainObject.getId(); | ||||
|             return !composition || (composition.indexOf(id) === -1); | ||||
|         } | ||||
|  | ||||
|         function navigateToParent(domainObject) { | ||||
|             var parent = getParent(domainObject); | ||||
|             return parent.getCapability('action').perform('navigate'); | ||||
|         } | ||||
|  | ||||
|         function checkNavigation() { | ||||
|             var navigatedObject = navigationService.getNavigation(); | ||||
|             if (navigatedObject.hasCapability('context') && | ||||
|                 isOrphan(navigatedObject)) { | ||||
|                 if (!navigatedObject.getCapability('editor').isEditContextRoot()) { | ||||
|                     navigateToParent(navigatedObject); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         throttledCheckNavigation = throttle(checkNavigation); | ||||
|  | ||||
|         navigationService.addListener(throttledCheckNavigation); | ||||
|         topic('mutation').listen(throttledCheckNavigation); | ||||
|     } | ||||
|  | ||||
|     return OrphanNavigationHandler; | ||||
| }); | ||||
| @@ -37,9 +37,8 @@ define( | ||||
|                 mockUrlService, | ||||
|                 mockDomainObject, | ||||
|                 mockNextObject, | ||||
|                 mockWindow, | ||||
|                 mockPolicyService, | ||||
|                 testDefaultRoot, | ||||
|                 mockActionCapability, | ||||
|                 controller; | ||||
|  | ||||
|             function mockPromise(value) { | ||||
| @@ -55,25 +54,14 @@ 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( | ||||
| @@ -128,6 +116,8 @@ define( | ||||
|                 mockNextObject.getId.andReturn("next"); | ||||
|                 mockDomainObject.getId.andReturn(testDefaultRoot); | ||||
|  | ||||
|                 mockActionCapability = jasmine.createSpyObj('actionCapability', ['perform']); | ||||
|  | ||||
|                 instantiateController(); | ||||
|             }); | ||||
|  | ||||
| @@ -211,8 +201,13 @@ define( | ||||
|                 mockContext.getPath.andReturn( | ||||
|                     [mockRootObject, mockDomainObject, mockNextObject] | ||||
|                 ); | ||||
|  | ||||
|                 //Return true from navigate action | ||||
|                 mockActionCapability.perform.andReturn(mockPromise(true)); | ||||
|  | ||||
|                 mockNextObject.getCapability.andCallFake(function (c) { | ||||
|                     return c === 'context' && mockContext; | ||||
|                     return (c === 'context' && mockContext) || | ||||
|                         (c === 'action' && mockActionCapability); | ||||
|                 }); | ||||
|                 mockScope.$on.andReturn(mockUnlisten); | ||||
|                 // Provide a navigation change | ||||
| @@ -225,6 +220,7 @@ define( | ||||
|                 mockLocation.path.andReturn("/browse/"); | ||||
|  | ||||
|                 mockNavigationService.setNavigation.andReturn(true); | ||||
|                 mockActionCapability.perform.andReturn(mockPromise(true)); | ||||
|  | ||||
|                 // Exercise the Angular workaround | ||||
|                 mockNavigationService.addListener.mostRecentCall.args[0](); | ||||
| @@ -243,6 +239,9 @@ define( | ||||
|                 mockScope.navigatedObject = mockDomainObject; | ||||
|                 mockNavigationService.setNavigation.andReturn(true); | ||||
|  | ||||
|                 mockActionCapability.perform.andReturn(mockPromise(true)); | ||||
|                 mockNextObject.getCapability.andReturn(mockActionCapability); | ||||
|  | ||||
|                 //Simulate a change in selected tree object | ||||
|                 mockScope.treeModel = {selectedObject: mockDomainObject}; | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockNextObject); | ||||
| @@ -254,11 +253,10 @@ define( | ||||
|             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; | ||||
|                 }); | ||||
|  | ||||
|                 //Return false from navigation action | ||||
|                 mockActionCapability.perform.andReturn(mockPromise(false)); | ||||
|                 mockNextObject.getCapability.andReturn(mockActionCapability); | ||||
|  | ||||
|                 //Simulate a change in selected tree object | ||||
|                 mockScope.treeModel = {selectedObject: mockDomainObject}; | ||||
|   | ||||
| @@ -31,6 +31,8 @@ define( | ||||
|             var mockNavigationService, | ||||
|                 mockQ, | ||||
|                 mockDomainObject, | ||||
|                 mockPolicyService, | ||||
|                 mockWindow, | ||||
|                 action; | ||||
|  | ||||
|             function mockPromise(value) { | ||||
| @@ -44,25 +46,70 @@ define( | ||||
|             beforeEach(function () { | ||||
|                 mockNavigationService = jasmine.createSpyObj( | ||||
|                     "navigationService", | ||||
|                     ["setNavigation"] | ||||
|                     [ | ||||
|                         "setNavigation", | ||||
|                         "getNavigation" | ||||
|                     ] | ||||
|                 ); | ||||
|                 mockNavigationService.getNavigation.andReturn({}); | ||||
|                 mockQ = { when: mockPromise }; | ||||
|                 mockDomainObject = jasmine.createSpyObj( | ||||
|                     "domainObject", | ||||
|                     ["getId", "getModel", "getCapability"] | ||||
|                 ); | ||||
|  | ||||
|                 mockPolicyService = jasmine.createSpyObj("policyService", | ||||
|                     [ | ||||
|                         "allow" | ||||
|                     ]); | ||||
|                 mockWindow = jasmine.createSpyObj("$window", | ||||
|                     [ | ||||
|                         "confirm" | ||||
|                     ]); | ||||
|  | ||||
|                 action = new NavigateAction( | ||||
|                     mockNavigationService, | ||||
|                     mockQ, | ||||
|                     mockPolicyService, | ||||
|                     mockWindow, | ||||
|                     { domainObject: mockDomainObject } | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("invokes the navigate service when performed", function () { | ||||
|             it("invokes the policy service to determine if navigation" + | ||||
|                 " allowed", function () { | ||||
|                 action.perform(); | ||||
|                 expect(mockNavigationService.setNavigation) | ||||
|                     .toHaveBeenCalledWith(mockDomainObject); | ||||
|                 expect(mockPolicyService.allow) | ||||
|                     .toHaveBeenCalledWith("navigation", jasmine.any(Object), jasmine.any(Object), jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             it("prompts user if policy rejection", function () { | ||||
|                 action.perform(); | ||||
|                 expect(mockPolicyService.allow).toHaveBeenCalled(); | ||||
|                 mockPolicyService.allow.mostRecentCall.args[3](); | ||||
|                 expect(mockWindow.confirm).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             describe("shows a prompt", function () { | ||||
|                 beforeEach(function () { | ||||
|                     // Ensure the allow callback is called synchronously | ||||
|                     mockPolicyService.allow.andCallFake(function () { | ||||
|                         return arguments[3](); | ||||
|                     }); | ||||
|                 }); | ||||
|                 it("does not navigate on prompt rejection", function () { | ||||
|                     mockWindow.confirm.andReturn(false); | ||||
|                     action.perform(); | ||||
|                     expect(mockNavigationService.setNavigation) | ||||
|                         .not.toHaveBeenCalled(); | ||||
|                 }); | ||||
|  | ||||
|                 it("does navigate on prompt acceptance", function () { | ||||
|                     mockWindow.confirm.andReturn(true); | ||||
|                     action.perform(); | ||||
|                     expect(mockNavigationService.setNavigation) | ||||
|                         .toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("is only applicable when a domain object is in context", function () { | ||||
|   | ||||
| @@ -0,0 +1,180 @@ | ||||
| /***************************************************************************** | ||||
| * 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. | ||||
| *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     '../../src/navigation/OrphanNavigationHandler' | ||||
| ], function (OrphanNavigationHandler) { | ||||
|     describe("OrphanNavigationHandler", function () { | ||||
|         var mockTopic, | ||||
|             mockThrottle, | ||||
|             mockMutationTopic, | ||||
|             mockNavigationService, | ||||
|             mockDomainObject, | ||||
|             mockParentObject, | ||||
|             mockContext, | ||||
|             mockActionCapability, | ||||
|             mockEditor, | ||||
|             testParentModel, | ||||
|             testId, | ||||
|             mockThrottledFns; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             testId = 'some-identifier'; | ||||
|  | ||||
|             mockThrottledFns = []; | ||||
|             testParentModel = {}; | ||||
|  | ||||
|             mockTopic = jasmine.createSpy('topic'); | ||||
|             mockThrottle = jasmine.createSpy('throttle'); | ||||
|             mockNavigationService = jasmine.createSpyObj('navigationService', [ | ||||
|                 'getNavigation', | ||||
|                 'addListener' | ||||
|             ]); | ||||
|             mockMutationTopic = jasmine.createSpyObj('mutationTopic', [ | ||||
|                 'listen' | ||||
|             ]); | ||||
|             mockDomainObject = jasmine.createSpyObj('domainObject', [ | ||||
|                 'getId', | ||||
|                 'getCapability', | ||||
|                 'getModel', | ||||
|                 'hasCapability' | ||||
|             ]); | ||||
|             mockParentObject = jasmine.createSpyObj('domainObject', [ | ||||
|                 'getId', | ||||
|                 'getCapability', | ||||
|                 'getModel', | ||||
|                 'hasCapability' | ||||
|             ]); | ||||
|             mockContext = jasmine.createSpyObj('context', ['getParent']); | ||||
|             mockActionCapability = jasmine.createSpyObj('action', ['perform']); | ||||
|             mockEditor = jasmine.createSpyObj('editor', ['isEditContextRoot']); | ||||
|  | ||||
|             mockThrottle.andCallFake(function (fn) { | ||||
|                 var mockThrottledFn = | ||||
|                     jasmine.createSpy('throttled-' + mockThrottledFns.length); | ||||
|                 mockThrottledFn.andCallFake(fn); | ||||
|                 mockThrottledFns.push(mockThrottledFn); | ||||
|                 return mockThrottledFn; | ||||
|             }); | ||||
|             mockTopic.andCallFake(function (k) { | ||||
|                 return k === 'mutation' && mockMutationTopic; | ||||
|             }); | ||||
|             mockDomainObject.getId.andReturn(testId); | ||||
|             mockDomainObject.getCapability.andCallFake(function (c) { | ||||
|                 return { | ||||
|                     context: mockContext, | ||||
|                     editor: mockEditor | ||||
|                 }[c]; | ||||
|             }); | ||||
|             mockDomainObject.hasCapability.andCallFake(function (c) { | ||||
|                 return !!mockDomainObject.getCapability(c); | ||||
|             }); | ||||
|             mockParentObject.getModel.andReturn(testParentModel); | ||||
|             mockParentObject.getCapability.andCallFake(function (c) { | ||||
|                 return { | ||||
|                     action: mockActionCapability | ||||
|                 }[c]; | ||||
|             }); | ||||
|             mockContext.getParent.andReturn(mockParentObject); | ||||
|             mockNavigationService.getNavigation.andReturn(mockDomainObject); | ||||
|             mockEditor.isEditContextRoot.andReturn(false); | ||||
|  | ||||
|             return new OrphanNavigationHandler( | ||||
|                 mockThrottle, | ||||
|                 mockTopic, | ||||
|                 mockNavigationService | ||||
|             ); | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         it("listens for mutation with a throttled function", function () { | ||||
|             expect(mockMutationTopic.listen) | ||||
|                 .toHaveBeenCalledWith(jasmine.any(Function)); | ||||
|             expect(mockThrottledFns.indexOf( | ||||
|                 mockMutationTopic.listen.mostRecentCall.args[0] | ||||
|             )).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("listens for navigation changes with a throttled function", function () { | ||||
|             expect(mockNavigationService.addListener) | ||||
|                 .toHaveBeenCalledWith(jasmine.any(Function)); | ||||
|             expect(mockThrottledFns.indexOf( | ||||
|                 mockNavigationService.addListener.mostRecentCall.args[0] | ||||
|             )).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         [false, true].forEach(function (isOrphan) { | ||||
|             var prefix = isOrphan ? "" : "non-"; | ||||
|             describe("for " + prefix + "orphan objects", function () { | ||||
|                 beforeEach(function () { | ||||
|                     testParentModel.composition = isOrphan ? [] : [testId]; | ||||
|                 }); | ||||
|  | ||||
|                 [false, true].forEach(function (isEditRoot) { | ||||
|                     var caseName = isEditRoot ? | ||||
|                         "that are being edited" : "that are not being edited"; | ||||
|  | ||||
|                     function itNavigatesAsExpected() { | ||||
|                         if (isOrphan && !isEditRoot) { | ||||
|                             it("navigates to the parent", function () { | ||||
|                                 expect(mockActionCapability.perform) | ||||
|                                     .toHaveBeenCalledWith('navigate'); | ||||
|                             }); | ||||
|                         } else { | ||||
|                             it("does nothing", function () { | ||||
|                                 expect(mockActionCapability.perform) | ||||
|                                     .not.toHaveBeenCalled(); | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     describe(caseName, function () { | ||||
|                         beforeEach(function () { | ||||
|                             mockEditor.isEditContextRoot.andReturn(isEditRoot); | ||||
|                         }); | ||||
|  | ||||
|                         describe("when navigation changes", function () { | ||||
|                             beforeEach(function () { | ||||
|                                 mockNavigationService.addListener.mostRecentCall | ||||
|                                     .args[0](mockDomainObject); | ||||
|                             }); | ||||
|  | ||||
|                             itNavigatesAsExpected(); | ||||
|                         }); | ||||
|  | ||||
|                         describe("when mutation occurs", function () { | ||||
|                             beforeEach(function () { | ||||
|                                 mockMutationTopic.listen.mostRecentCall | ||||
|                                     .args[0](mockParentObject); | ||||
|                             }); | ||||
|  | ||||
|                             itNavigatesAsExpected(); | ||||
|                         }); | ||||
|  | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @@ -170,7 +170,7 @@ define([ | ||||
|                         "navigationService", | ||||
|                         "$log" | ||||
|                     ], | ||||
|                     "description": "Edit this object.", | ||||
|                     "description": "Edit", | ||||
|                     "category": "view-control", | ||||
|                     "glyph": "p" | ||||
|                 }, | ||||
|   | ||||
| @@ -50,18 +50,24 @@ define( | ||||
|                 //If the object existed already, navigate to refresh view | ||||
|                 // with previous object state. | ||||
|                 if (domainObject.getModel().persisted) { | ||||
|                     domainObject.getCapability("action").perform("navigate"); | ||||
|                     return domainObject.getCapability("action").perform("navigate"); | ||||
|                 } else { | ||||
|                     //If the object was new, and user has cancelled, then | ||||
|                     //navigate back to parent because nothing to show. | ||||
|                     domainObject.getCapability("location").getOriginal().then(function (original) { | ||||
|                     return domainObject.getCapability("location").getOriginal().then(function (original) { | ||||
|                         parent = original.getCapability("context").getParent(); | ||||
|                         parent.getCapability("action").perform("navigate"); | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|             return this.domainObject.getCapability("editor").cancel() | ||||
|                 .then(returnToBrowse); | ||||
|  | ||||
|             function cancel(allowed) { | ||||
|                 return allowed && domainObject.getCapability("editor").cancel(); | ||||
|             } | ||||
|  | ||||
|             //Do navigation first in order to trigger unsaved changes dialog | ||||
|             return returnToBrowse() | ||||
|                 .then(cancel); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -67,10 +67,17 @@ define( | ||||
|             } | ||||
|  | ||||
|             function onCancel() { | ||||
|                 return self.persistenceCapability.refresh().then(function (result) { | ||||
|                 if (self.domainObject.getModel().persisted !== undefined) { | ||||
|                     //Fetch clean model from persistence | ||||
|                     return self.persistenceCapability.refresh().then(function (result) { | ||||
|                         self.persistPending = false; | ||||
|                         return result; | ||||
|                     }); | ||||
|                 } else { | ||||
|                     self.persistPending = false; | ||||
|                     return result; | ||||
|                 }); | ||||
|                     //Model is undefined in persistence, so return undefined. | ||||
|                     return self.$q.when(undefined); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (this.transactionService.isActive()) { | ||||
|   | ||||
| @@ -24,12 +24,11 @@ define( | ||||
|     ["../../src/actions/CancelAction"], | ||||
|     function (CancelAction) { | ||||
|  | ||||
|         //TODO: Disabled for NEM Beta | ||||
|         xdescribe("The Cancel action", function () { | ||||
|             var mockLocation, | ||||
|                 mockDomainObject, | ||||
|                 mockEditorCapability, | ||||
|                 mockUrlService, | ||||
|         describe("The Cancel action", function () { | ||||
|             var mockDomainObject, | ||||
|                 mockParentObject, | ||||
|                 capabilities = {}, | ||||
|                 parentCapabilities = {}, | ||||
|                 actionContext, | ||||
|                 action; | ||||
|  | ||||
| @@ -42,61 +41,114 @@ define( | ||||
|             } | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockLocation = jasmine.createSpyObj( | ||||
|                     "$location", | ||||
|                     ["path"] | ||||
|                 ); | ||||
|                 mockDomainObject = jasmine.createSpyObj( | ||||
|                     "domainObject", | ||||
|                     ["getCapability", "hasCapability"] | ||||
|                     [ | ||||
|                         "getCapability", | ||||
|                         "hasCapability", | ||||
|                         "getModel" | ||||
|                     ] | ||||
|                 ); | ||||
|                 mockEditorCapability = jasmine.createSpyObj( | ||||
|                 mockDomainObject.getModel.andReturn({}); | ||||
|  | ||||
|                 mockParentObject = jasmine.createSpyObj( | ||||
|                     "parentObject", | ||||
|                     [ | ||||
|                         "getCapability" | ||||
|                     ] | ||||
|                 ); | ||||
|                 mockParentObject.getCapability.andCallFake(function (name) { | ||||
|                     return parentCapabilities[name]; | ||||
|                 }); | ||||
|  | ||||
|                 capabilities.editor = jasmine.createSpyObj( | ||||
|                     "editor", | ||||
|                     ["save", "cancel"] | ||||
|                     ["save", "cancel", "isEditContextRoot"] | ||||
|                 ); | ||||
|                 mockUrlService = jasmine.createSpyObj( | ||||
|                     "urlService", | ||||
|                     ["urlForLocation"] | ||||
|                 capabilities.action = jasmine.createSpyObj( | ||||
|                     "actionCapability", | ||||
|                     [ | ||||
|                         "perform" | ||||
|                     ] | ||||
|                 ); | ||||
|                 capabilities.location = jasmine.createSpyObj( | ||||
|                     "locationCapability", | ||||
|                     [ | ||||
|                         "getOriginal" | ||||
|                     ] | ||||
|                 ); | ||||
|                 capabilities.location.getOriginal.andReturn(mockPromise(mockDomainObject)); | ||||
|                 capabilities.context = jasmine.createSpyObj( | ||||
|                     "contextCapability", | ||||
|                     [ | ||||
|                         "getParent" | ||||
|                     ] | ||||
|                 ); | ||||
|                 capabilities.context.getParent.andReturn(mockParentObject); | ||||
|  | ||||
|                 parentCapabilities.action = jasmine.createSpyObj( | ||||
|                     "actionCapability", | ||||
|                     [ | ||||
|                         "perform" | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 actionContext = { | ||||
|                     domainObject: mockDomainObject | ||||
|                 }; | ||||
|  | ||||
|                 mockDomainObject.hasCapability.andReturn(true); | ||||
|                 mockDomainObject.getCapability.andReturn(mockEditorCapability); | ||||
|                 mockEditorCapability.cancel.andReturn(mockPromise(true)); | ||||
|                 mockDomainObject.getCapability.andCallFake(function (name) { | ||||
|                     return capabilities[name]; | ||||
|                 }); | ||||
|  | ||||
|                 action = new CancelAction(mockLocation, mockUrlService, actionContext); | ||||
|                 mockDomainObject.hasCapability.andCallFake(function (name) { | ||||
|                     return !!capabilities[name]; | ||||
|                 }); | ||||
|  | ||||
|                 capabilities.editor.cancel.andReturn(mockPromise(true)); | ||||
|  | ||||
|                 action = new CancelAction(actionContext); | ||||
|  | ||||
|             }); | ||||
|  | ||||
|             it("only applies to domain object with an editor capability", function () { | ||||
|             it("only applies to domain object that is being edited", function () { | ||||
|                 capabilities.editor.isEditContextRoot.andReturn(true); | ||||
|                 expect(CancelAction.appliesTo(actionContext)).toBeTruthy(); | ||||
|                 expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor"); | ||||
|  | ||||
|                 capabilities.editor.isEditContextRoot.andReturn(false); | ||||
|                 expect(CancelAction.appliesTo(actionContext)).toBeFalsy(); | ||||
|  | ||||
|                 mockDomainObject.hasCapability.andReturn(false); | ||||
|                 mockDomainObject.getCapability.andReturn(undefined); | ||||
|                 expect(CancelAction.appliesTo(actionContext)).toBeFalsy(); | ||||
|             }); | ||||
|  | ||||
|             it("invokes the editor capability's save functionality when performed", function () { | ||||
|                 // Verify precondition | ||||
|                 expect(mockEditorCapability.cancel).not.toHaveBeenCalled(); | ||||
|             it("invokes the editor capability's cancel functionality when" + | ||||
|                 " performed", function () { | ||||
|                 mockDomainObject.getModel.andReturn({persisted: 1}); | ||||
|                 //Return true from navigate action | ||||
|                 capabilities.action.perform.andReturn(mockPromise(true)); | ||||
|                 action.perform(); | ||||
|  | ||||
|                 // Should have called cancel | ||||
|                 expect(mockEditorCapability.cancel).toHaveBeenCalled(); | ||||
|                 expect(capabilities.editor.cancel).toHaveBeenCalled(); | ||||
|  | ||||
|                 // Definitely shouldn't call save! | ||||
|                 expect(mockEditorCapability.save).not.toHaveBeenCalled(); | ||||
|                 expect(capabilities.editor.save).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("returns to browse when performed", function () { | ||||
|             it("navigates to object if existing using navigate action", function () { | ||||
|                 mockDomainObject.getModel.andReturn({persisted: 1}); | ||||
|                 //Return true from navigate action | ||||
|                 capabilities.action.perform.andReturn(mockPromise(true)); | ||||
|                 action.perform(); | ||||
|                 expect(mockLocation.path).toHaveBeenCalledWith( | ||||
|                     mockUrlService.urlForLocation("browse", mockDomainObject) | ||||
|                 ); | ||||
|                 expect(capabilities.action.perform).toHaveBeenCalledWith("navigate"); | ||||
|             }); | ||||
|  | ||||
|             it("navigates to parent if new using navigate action", function () { | ||||
|                 mockDomainObject.getModel.andReturn({persisted: undefined}); | ||||
|                 action.perform(); | ||||
|                 expect(parentCapabilities.action.perform).toHaveBeenCalledWith("navigate"); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|   | ||||
| @@ -57,6 +57,15 @@ define( | ||||
|                 ); | ||||
|                 mockPersistence.persist.andReturn(fastPromise()); | ||||
|                 mockPersistence.refresh.andReturn(fastPromise()); | ||||
|  | ||||
|                 mockDomainObject = jasmine.createSpyObj( | ||||
|                     "domainObject", | ||||
|                     [ | ||||
|                         "getModel" | ||||
|                     ] | ||||
|                 ); | ||||
|                 mockDomainObject.getModel.andReturn({persisted: 1}); | ||||
|  | ||||
|                 capability = new TransactionalPersistenceCapability(mockQ, mockTransactionService, mockPersistence, mockDomainObject); | ||||
|             }); | ||||
|  | ||||
| @@ -78,6 +87,20 @@ define( | ||||
|                 expect(mockPersistence.refresh).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("if transaction is active, cancel call is queued that refreshes model when appropriate", function () { | ||||
|                 mockTransactionService.isActive.andReturn(true); | ||||
|                 capability.persist(); | ||||
|                 expect(mockTransactionService.addToTransaction).toHaveBeenCalled(); | ||||
|  | ||||
|                 mockDomainObject.getModel.andReturn({}); | ||||
|                 mockTransactionService.addToTransaction.mostRecentCall.args[1](); | ||||
|                 expect(mockPersistence.refresh).not.toHaveBeenCalled(); | ||||
|  | ||||
|                 mockDomainObject.getModel.andReturn({persisted: 1}); | ||||
|                 mockTransactionService.addToTransaction.mostRecentCall.args[1](); | ||||
|                 expect(mockPersistence.refresh).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("persist call is only added to transaction once", function () { | ||||
|                 mockTransactionService.isActive.andReturn(true); | ||||
|                 capability.persist(); | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -100,5 +100,6 @@ | ||||
| <glyph unicode="" glyph-name="icon-tabular-realtime" d="M896 960h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128zM448 668l25.060-25.32c7.916-7.922 18.856-12.822 30.94-12.822s23.024 4.9 30.94 12.822l75.5 76.3c29.97 30.338 71.571 49.128 117.56 49.128s87.59-18.79 117.544-49.112l50.456-50.997v-152.2c-24.111 8.83-44.678 22.255-61.542 39.342l-75.518 76.318c-7.916 7.922-18.856 12.822-30.94 12.822s-23.024-4.9-30.94-12.822l-75.5-76.3c-29.971-30.343-71.575-49.137-117.568-49.137-20.084 0-39.331 3.584-57.137 10.146l1.145 151.831zM320 0h-192c-35.26 0.214-63.786 28.74-64 63.98v128.020h256v-192zM320 256h-256v192h256v-192zM320 512h-256v192h256v-192zM640 0h-256v192h256v-192zM448 323.38v174.5c1.88-1.74 3.74-3.5 5.56-5.34l75.5-76.3c7.916-7.922 18.856-12.822 30.94-12.822s23.024 4.9 30.94 12.822l75.5 76.3c29.966 30.333 71.56 49.119 117.542 49.119 43.28 0 82.673-16.644 112.128-43.879l-0.11-174.399c-1.88 1.74-3.74 3.5-5.56 5.34l-75.5 76.3c-7.916 7.922-18.856 12.822-30.94 12.822s-23.024-4.9-30.94-12.822l-75.5-76.3c-29.966-30.333-71.56-49.119-117.542-49.119-43.28 0-82.673 16.644-112.128 43.879zM960 64c-0.214-35.26-28.74-63.786-63.98-64h-192.020v192h256v-128z" /> | ||||
| <glyph unicode="" glyph-name="icon-tabular-lad" d="M896 960h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128zM64 704h256v-192h-256v192zM64 448h256v-192h-256v192zM128 0c-35.26 0.214-63.786 28.74-64 63.98v128.020h256v-192h-192zM384 0v192h256v-192h-256zM960 64c-0.214-35.26-28.74-63.786-63.98-64h-192.020v192h256v-128zM960 448v-192h-576v192h64v64h-64v192h576v-192h-64v-64h64zM782.32 412.62l-110.32 55.16v172.22c0 17.673-14.327 32-32 32s-32-14.327-32-32v-211.78l145.68-72.84c4.172-2.133 9.1-3.383 14.32-3.383 17.675 0 32.003 14.328 32.003 32.003 0 12.454-7.114 23.247-17.501 28.536z" /> | ||||
| <glyph unicode="" glyph-name="icon-tabular-lad-set" d="M128 192v576c-70.606-0.215-127.785-57.394-128-127.979v-576.021c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979l-576 0.021c-70.606 0.215-127.785 57.394-128 127.979zM896 960h-576c-70.606-0.215-127.785-57.394-128-127.979v-576.021c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v576.021c-0.215 70.606-57.394 127.785-127.979 128zM256 768h192v-128h-192v128zM256 576h192v-192h-192v192zM320 192c-35.26 0.214-63.786 28.74-64 63.98v64.020h192v-128h-128zM512 192v128h192v-128h-192zM960 256c-0.214-35.26-28.74-63.786-63.98-64h-128.020v128h192v-64zM960 384h-448v384h448v-384zM832 480c0.002 0 0.005 0 0.007 0 17.673 0 32 14.327 32 32 0 14.055-9.062 25.994-21.662 30.293l-74.345 24.767v104.94c0 17.673-14.327 32-32 32s-32-14.327-32-32v-151.060l117.88-39.3c3.018-1.040 6.495-1.64 10.113-1.64 0.003 0 0.005 0 0.008 0z" /> | ||||
| <glyph unicode="" glyph-name="icon-download" d="M832 384v-255.66l-0.34-0.34-639.66 0.34v255.66h-192v-256c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v256h-192zM512 320l448 448h-256v192h-384v-192h-256l448-448z" /> | ||||
| <glyph unicode="" glyph-name="icon-x" d="M384 448l-365.332-365.332c-24.89-24.89-24.89-65.62 0-90.51l37.49-37.49c24.89-24.89 65.62-24.89 90.51 0 0 0 365.332 365.332 365.332 365.332l365.332-365.332c24.89-24.89 65.62-24.89 90.51 0l37.49 37.49c24.89 24.89 24.89 65.62 0 90.51l-365.332 365.332c0 0 365.332 365.332 365.332 365.332 24.89 24.89 24.89 65.62 0 90.51l-37.49 37.49c-24.89 24.89-65.62 24.89-90.51 0 0 0-365.332-365.332-365.332-365.332l-365.332 365.332c-24.89 24.89-65.62 24.89-90.51 0l-37.49-37.49c-24.89-24.89-24.89-65.62 0-90.51 0 0 365.332-365.332 365.332-365.332z" /> | ||||
| </font></defs></svg> | ||||
| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB | 
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -49,7 +49,7 @@ $uePaneMiniTabCollapsedW: 11px; | ||||
| $ueEditLeftPaneW: 75%; | ||||
| $treeSearchInputBarH: 25px; | ||||
| $ueTimeControlH: (33px, 18px, 20px); | ||||
| // Panes | ||||
| /*************** Panes */ | ||||
| $ueBrowseLeftPaneTreeMinW: 150px; | ||||
| $ueBrowseLeftPaneTreeMaxW: 35%; | ||||
| $ueBrowseLeftPaneTreeW: 25%; | ||||
| @@ -57,48 +57,58 @@ $ueBrowseRightPaneInspectMinW: 200px; | ||||
| $ueBrowseRightPaneInspectMaxW: 35%; | ||||
| $ueBrowseRightPaneInspectW: 20%; | ||||
| $ueDesktopMinW: 600px; | ||||
|  | ||||
| // Overlay | ||||
| /*************** Overlay */ | ||||
| $ovrTopBarH: 45px; | ||||
| $ovrFooterH: 24px; | ||||
| $overlayMargin: 25px; | ||||
| // Items | ||||
| /*************** Items */ | ||||
| $ueBrowseGridItemLg: 200px; | ||||
| $ueBrowseGridItemTopBarH: 20px; | ||||
| $ueBrowseGridItemBottomBarH: 30px; | ||||
| $itemPadLR: 5px; | ||||
| // Tree | ||||
| /*************** Tree */ | ||||
| $treeVCW: 10px; | ||||
| $treeTypeIconH: 1.4em; // was 16px | ||||
| $treeTypeIconHPx: 16px; | ||||
| $treeTypeIconW: 18px; | ||||
| $treeContextTriggerW: 20px; | ||||
| // Tabular | ||||
| /*************** Tabular */ | ||||
| $tabularHeaderH: 22px; //18px | ||||
| $tabularTdPadLR: $itemPadLR; | ||||
| $tabularTdPadTB: 3px; | ||||
| // Imagery | ||||
| /*************** Imagery */ | ||||
| $imageMainControlBarH: 25px; | ||||
| $imageThumbsD: 120px; | ||||
| $imageThumbsWrapperH: $imageThumbsD * 1.4; | ||||
| $imageThumbPad: 1px; | ||||
| // Ticks | ||||
| /*************** Ticks */ | ||||
| $ticksH: 25px; | ||||
| $tickLblVMargin: 3px; | ||||
| $tickLblH: 15px; | ||||
| $tickLblW: 50px; | ||||
| $tickH: $ticksH - $tickLblVMargin - $tickLblH; | ||||
| $tickW: 1px; | ||||
| // Bubbles | ||||
| /*************** Plots */ | ||||
| $plotYBarW: 60px; | ||||
| $plotYLabelMinH: 20px; | ||||
| $plotYLabelW: 10px; | ||||
| $plotXBarH: 32px; | ||||
| $plotLegendH: 20px; | ||||
| $plotSwatchD: 8px; | ||||
| // 1: Top, 2: right, 3: bottom, 4: left | ||||
| $plotDisplayArea: ($plotLegendH + $interiorMargin, 0, $plotXBarH + $interiorMargin, $plotYBarW); | ||||
| /* min plot height is based on user testing to find minimum useful height */ | ||||
| $plotMinH: 95px; | ||||
| /*************** Bubbles */ | ||||
| $bubbleArwSize: 10px; | ||||
| $bubblePad: $interiorMargin; | ||||
| $bubbleMinW: 100px; | ||||
| $bubbleMaxW: 300px; | ||||
| // Forms | ||||
| /*************** Forms */ | ||||
| $reqSymbolW: 15px; | ||||
| $reqSymbolM: $interiorMargin * 2; | ||||
| $reqSymbolFontSize: 0.7em; | ||||
| // Wait Spinner Defaults | ||||
| /*************** Wait Spinner Defaults */ | ||||
| $waitSpinnerD: 32px; | ||||
| $waitSpinnerTreeD: 20px; | ||||
| $waitSpinnerBorderW: 5px; | ||||
| @@ -124,6 +134,8 @@ $dirImgs: $dirCommonRes + 'images/'; | ||||
|  | ||||
| /************************** TIMINGS */ | ||||
| $controlFadeMs: 100ms; | ||||
| $browseToEditAnimMs: 400ms; | ||||
| $editBorderPulseMs: 500ms; | ||||
|  | ||||
| /************************** LIMITS */ | ||||
| $glyphLimit: '\e603'; | ||||
|   | ||||
| @@ -39,15 +39,20 @@ | ||||
|     @include pulse($animName: pulse-subtle, $dur: 500ms, $opacity0: 0.7); | ||||
| } | ||||
|  | ||||
| @mixin pulseBorder($c: red, $dur: 500ms, $iteration: infinite, $delay: 0s, $opacity0: 0, $opacity100: 1) { | ||||
|     @include keyframes(pulseBorder) { | ||||
|         0%   { border-color: rgba($c, $opacity0); } | ||||
|         100% { border-color: rgba($c, $opacity100); } | ||||
| @mixin animTo($animName, $propName, $propValStart, $propValEnd, $dur: 500ms, $delay: 0) { | ||||
|     @include keyframes($animName) { | ||||
|         from { #{propName}: $propValStart; } | ||||
|         to { #{$propName}: $propValEnd; } | ||||
|     } | ||||
|     @include animation-name(pulseBorder); | ||||
|     @include animation-duration($dur); | ||||
|     @include animation-direction(alternate); | ||||
|     @include animation-iteration-count($iteration); | ||||
|     @include animation-timing-function(ease); | ||||
|     @include animation-delay($delay); | ||||
|     @include animToParams($animName, $dur: 500ms, $delay: 0) | ||||
| } | ||||
|  | ||||
| @mixin animToParams($animName, $dur: 500ms, $delay: 0) { | ||||
|     @include animation-name($animName); | ||||
|     @include animation-duration($dur); | ||||
|     @include animation-delay($delay); | ||||
|     @include animation-fill-mode(both); | ||||
|     @include animation-direction(normal); | ||||
|     @include animation-iteration-count(1); | ||||
|     @include animation-timing-function(ease-in-out); | ||||
| } | ||||
| @@ -348,7 +348,6 @@ | ||||
| 	display: inline-block; | ||||
| 	font-family: 'symbolsfont'; | ||||
| 	margin-left: $interiorMarginSm; | ||||
| 	vertical-align: top; | ||||
| } | ||||
|  | ||||
| @mixin nice-textarea($bg: $colorBodyBg, $fg: $colorBodyFg) { | ||||
|   | ||||
| @@ -36,15 +36,7 @@ $pad: $interiorMargin * $baseRatio; | ||||
|     padding: 0 $pad; | ||||
|     font-size: 0.7rem; | ||||
|     vertical-align: top; | ||||
|  | ||||
|     .icon { | ||||
|         font-size: 0.8rem; | ||||
|         color: $colorKey; | ||||
|     } | ||||
|  | ||||
|     .title-label { | ||||
|         vertical-align: top; | ||||
|     } | ||||
|     @include btnSubtle($colorBtnBg, $colorBtnBgHov, $colorBtnFg, $colorBtnIcon); | ||||
|  | ||||
| 	&.lg { | ||||
| 		font-size: 1rem; | ||||
| @@ -58,19 +50,14 @@ $pad: $interiorMargin * $baseRatio; | ||||
|         padding: 0 ($pad / $baseRatio) / 2; | ||||
|     } | ||||
|  | ||||
|     &.major { | ||||
|     &.major, | ||||
|     &.key-edit, | ||||
|     &.key-properties { | ||||
|         $bg: $colorBtnMajorBg; | ||||
|         $hc: lighten($bg, 10%); | ||||
|         @include btnSubtle($bg, $hc, $colorBtnMajorFg, $colorBtnMajorFg); | ||||
|     } | ||||
|  | ||||
|     &:not(.major) { | ||||
|         // bg, bgHov, fg, ic | ||||
|         @include btnSubtle($colorBtnBg, $colorBtnBgHov, $colorBtnFg, $colorBtnIcon); | ||||
|     } | ||||
|     &.pause-play { | ||||
|  | ||||
|     } | ||||
|     &.t-save:before { | ||||
|         content:'\e612'; | ||||
|         font-family: symbolsfont; | ||||
| @@ -109,6 +96,22 @@ $pad: $interiorMargin * $baseRatio; | ||||
|             content: "\000039"; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.t-export { | ||||
|         &:before { | ||||
|             @extend .ui-symbol; | ||||
|             @extend .icon; | ||||
|             content: '\e623'; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .icon { | ||||
|         font-size: 0.8rem; | ||||
|     } | ||||
|  | ||||
|     .title-label { | ||||
|         vertical-align: top; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .s-icon-btn { | ||||
| @@ -275,4 +278,3 @@ body.desktop .mini-tab-icon { | ||||
|         color: $colorPausedBg !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,10 @@ | ||||
| .l-image-main-wrapper, | ||||
| .l-image-main, | ||||
| .l-image-main-controlbar, | ||||
| .l-image-thumbs-wrapper { | ||||
| 	@include absPosDefault(0, false); | ||||
| } | ||||
|  | ||||
| /*************************************** MAIN LAYOUT */ | ||||
| .l-image-main-wrapper { | ||||
| 	//@include test(); | ||||
| 	@if $enableImageryThumbs == true { | ||||
| 		bottom: $interiorMargin*2 +  $imageThumbsWrapperH; | ||||
| 	} | ||||
| @@ -15,16 +12,14 @@ | ||||
| 	min-width: 150px; | ||||
| 	.l-image-main { | ||||
| 		background-color: $colorPlotBg; | ||||
| 		bottom: $imageMainControlBarH + $interiorMargin; | ||||
|         margin-bottom: $interiorMargin; | ||||
| 	} | ||||
| 	.l-image-main-controlbar { | ||||
| 		top: auto; | ||||
| 		height: $imageMainControlBarH; | ||||
|         &.l-flex-row { @include align-items(center); } | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .l-image-thumbs-wrapper { | ||||
| 	//@include test(red); | ||||
| 	top: auto; | ||||
| 	height: $imageThumbsWrapperH; | ||||
| } | ||||
| @@ -44,24 +39,17 @@ | ||||
| 	background-repeat: no-repeat; | ||||
| } | ||||
|  | ||||
| .l-image-main { | ||||
| 	//cursor: crosshair; | ||||
| } | ||||
|  | ||||
| .l-image-main-controlbar { | ||||
| 	//@include test(); | ||||
| 	font-size: 0.8em; | ||||
| 	line-height: $imageMainControlBarH; | ||||
| 	line-height: inherit; | ||||
| 	.left, .right { | ||||
| 		direction: rtl; | ||||
| 		overflow: hidden; | ||||
| 	} | ||||
| 	.left { | ||||
| 		//@include test(red); | ||||
| 		text-align: left; | ||||
| 	} | ||||
| 	.right { | ||||
| 		//@include test(green); | ||||
| 		z-index: 2; | ||||
| 	} | ||||
| 	.l-date, | ||||
| @@ -71,7 +59,6 @@ | ||||
| 	.l-mag { | ||||
| 		direction: ltr; | ||||
| 		display: inline-block; | ||||
| 		//white-space: nowrap; | ||||
| 		&:before { | ||||
| 			content: "\000049"; | ||||
| 		} | ||||
|   | ||||
| @@ -24,6 +24,10 @@ | ||||
| 	height: 100%; | ||||
| } | ||||
|  | ||||
| .tabular-holder { | ||||
|     @include absPosDefault(); | ||||
| } | ||||
|  | ||||
| .tabular, | ||||
| table { | ||||
| 	box-sizing: border-box; | ||||
| @@ -162,4 +166,41 @@ table { | ||||
| 			min-width: 150px; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /********************************************************** SPECIFIC TABULAR VIEWS */ | ||||
| .tabular-holder { | ||||
|     &.t-exportable { | ||||
|         $btnExportH: 25px; | ||||
|         .l-view-section { | ||||
|             top: $btnExportH + $interiorMargin; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .child-frame { | ||||
|     .tabular-holder { | ||||
|         &.t-exportable { | ||||
|             $btnExportH: $btnFrameH; | ||||
|             .s-btn.t-export { | ||||
|                 @include trans-prop-nice(opacity, $dur: 50ms); | ||||
|                 opacity: 0; | ||||
|             } | ||||
|             .l-view-section { | ||||
|                 @include trans-prop-nice(top, $dur: 150ms, $delay: 50ms); | ||||
|                 top: 0; | ||||
|             } | ||||
|             &:hover { | ||||
|                 .s-btn.t-export { | ||||
|                     @include trans-prop-nice(opacity, 150ms, 100ms); | ||||
|                     opacity: 1; | ||||
|                 } | ||||
|                 .l-view-section { | ||||
|                     @include trans-prop-nice(top, $dur: 150ms); | ||||
|                     top: $btnExportH + $interiorMargin; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -19,12 +19,10 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| $yBarW: 60px; | ||||
| $yLabelW: 10px; | ||||
| $xBarH: 32px; | ||||
| $legendH: 20px; | ||||
| $swatchD: 8px; | ||||
| $plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBarW); // Top, right, bottom, left | ||||
| .abs.holder-plot { | ||||
|     // Fend off the scrollbar when less than min-height; | ||||
|     right: $interiorMargin; | ||||
| } | ||||
|  | ||||
| .gl-plot { | ||||
| 	color: $colorPlotFg; | ||||
| @@ -32,6 +30,7 @@ $plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBa | ||||
| 	position: relative; | ||||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
|     min-height: $plotMinH; | ||||
|  | ||||
|     .gl-plot-local-controls { | ||||
|         @include trans-prop-nice(opacity, 150ms); | ||||
| @@ -54,17 +53,17 @@ $plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBa | ||||
| 			top: auto; | ||||
| 			right: 0; | ||||
| 			bottom: $interiorMargin; | ||||
| 			left: $yBarW; | ||||
| 			height: $xBarH; | ||||
| 			left: $plotYBarW; | ||||
| 			height: $plotXBarH; | ||||
| 			width: auto; | ||||
| 			overflow: hidden; | ||||
| 		} | ||||
| 		&.gl-plot-y { | ||||
| 			top: $legendH + $interiorMargin; | ||||
| 			top: $plotLegendH + $interiorMargin; | ||||
| 			right: auto; | ||||
| 			bottom: nth($plotDisplayArea, 3); | ||||
| 			left: 0; | ||||
| 			width: $yBarW; | ||||
| 			width: $plotYBarW; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -146,7 +145,7 @@ $plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBa | ||||
|         @include transform(translateY(-50%)); | ||||
|         min-width: 150px; // Need this due to enclosure of .select | ||||
|         top: 50%; | ||||
|         left: $yLabelW + $interiorMargin * 2; | ||||
|         left: $plotYLabelW + $interiorMargin * 2; | ||||
|     } | ||||
|  | ||||
|     .t-plot-display-controls { | ||||
| @@ -174,7 +173,7 @@ $plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBa | ||||
| 		right: 0; | ||||
| 		bottom: auto; | ||||
| 		left: 0; | ||||
| 		height: $legendH; | ||||
| 		height: $plotLegendH; | ||||
| 		overflow-x: hidden; | ||||
| 		overflow-y: auto; | ||||
| 	} | ||||
| @@ -236,8 +235,8 @@ $plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBa | ||||
| 		.color-swatch { | ||||
| 			border-radius: 2px; | ||||
| 			display: inline-block; | ||||
| 			height: $swatchD; | ||||
| 			width: $swatchD; | ||||
| 			height: $plotSwatchD; | ||||
| 			width: $plotSwatchD; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -249,8 +248,8 @@ $plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBa | ||||
| 		padding: 0px $itemPadLR; | ||||
| 		.plot-color-swatch { | ||||
| 			border: 1px solid $colorBodyBg; | ||||
| 			height: $swatchD + 1; | ||||
| 			width: $swatchD + 1; | ||||
| 			height: $plotSwatchD + 1; | ||||
| 			width: $plotSwatchD + 1; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -54,7 +54,8 @@ | ||||
| 			height: $ohH; | ||||
| 			line-height: $ohH; | ||||
| 			padding: 0 $interiorMargin; | ||||
| 			> span { | ||||
| 			> span, | ||||
|             &:before { | ||||
| 				font-size: 0.65rem; | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -237,30 +237,10 @@ body.desktop .pane .mini-tab-icon.toggle-pane { | ||||
|     top: $ueTopBarH + $interiorMarginLg; | ||||
| } | ||||
|  | ||||
| .l-object-wrapper { | ||||
|     @extend .abs; | ||||
|  | ||||
|     .object-holder-main { | ||||
|         @extend .abs; | ||||
|     } | ||||
|     .l-edit-controls { | ||||
|         //@include trans-prop-nice((opacity, height), 0.25s); | ||||
|         border-bottom: 1px solid $colorInteriorBorder; | ||||
|         line-height: $ueEditToolBarH; | ||||
|         height: 0px; | ||||
|         opacity: 0; | ||||
|         .tool-bar { | ||||
|             right: $interiorMargin; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .l-object-wrapper-inner { | ||||
|     @include trans-prop-nice-resize(0.25s); | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| .object-browse-bar .s-btn, | ||||
| .top-bar .buttons-main .s-btn, | ||||
| .top-bar .s-menu-btn, | ||||
| @@ -288,8 +268,9 @@ body.desktop .pane .mini-tab-icon.toggle-pane { | ||||
|  | ||||
|     .left { | ||||
|         padding-right: $interiorMarginLg; | ||||
|         .l-back:not(.s-status-editing) { | ||||
|         .l-back { | ||||
|             margin-right: $interiorMarginLg; | ||||
|             &.s-status-editing { display: none; } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -376,19 +357,49 @@ body.desktop { | ||||
|  | ||||
| .s-status-editing { | ||||
|     .l-object-wrapper { | ||||
|         @include pulseBorder($colorEditAreaFg, $dur: 1s, $opacity0: 0.3); | ||||
|         border-radius: $controlCr; | ||||
|         $t2Dur: $browseToEditAnimMs; | ||||
|         $t1Dur: $t2Dur / 2; | ||||
|         $pulseDur: $editBorderPulseMs; | ||||
|         $bC0: rgba($colorEditAreaFg, 0.5); | ||||
|         $bC100: rgba($colorEditAreaFg, 1); | ||||
|  | ||||
|         background-color: $colorEditAreaBg; | ||||
|         border-color: $colorEditAreaFg; | ||||
|         border-width: 2px; | ||||
|         border-style: dotted; | ||||
|         .l-object-wrapper-inner { | ||||
|             @include absPosDefault(3px, hidden); | ||||
|         border-radius: $controlCr; | ||||
|         border: 1px dotted $bC0; | ||||
|  | ||||
|         // Transition 1 | ||||
|         @include keyframes(wrapperIn) { | ||||
|             from { border: 0px dotted transparent; padding: 0; } | ||||
|             to { border: 1px dotted $bC0; padding: 5px; } | ||||
|         } | ||||
|  | ||||
|         // Do last | ||||
|         @include keyframes(pulseNew) { | ||||
|             from  { border-color: $bC0; } | ||||
|             to { border-color: $bC100; } | ||||
|         } | ||||
|  | ||||
|         @include animation-name(wrapperIn, pulseNew); | ||||
|         @include animation-duration($t1Dur, $pulseDur); | ||||
|         @include animation-delay(0s, $t1Dur + $t2Dur); | ||||
|         @include animation-direction(normal, alternate); | ||||
|         @include animation-fill-mode(both, none); | ||||
|         @include animation-iteration-count(1, infinite); | ||||
|         @include animation-timing-function(ease-in-out, linear); | ||||
|  | ||||
|  | ||||
|         .l-edit-controls { | ||||
|             height: $ueEditToolBarH + $interiorMargin; | ||||
|             margin-bottom: $interiorMargin; | ||||
|             opacity: 1; | ||||
|             height: 0; | ||||
|             border-bottom: 1px solid $colorInteriorBorder; | ||||
|             // Transition 2: reveal edit controls | ||||
|             @include keyframes(editIn) { | ||||
|                 from { border-bottom: 0px solid transparent; height: 0; margin-bottom: 0; } | ||||
|                 to { border-bottom: 1px solid $colorInteriorBorder; height: $ueEditToolBarH + $interiorMargin; margin-bottom: $interiorMargin; } | ||||
|             } | ||||
|             @include animToParams(editIn, $dur: $t2Dur, $delay: $t1Dur); | ||||
|             .tool-bar { | ||||
|                 right: $interiorMargin; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -29,10 +29,12 @@ | ||||
|                                 !structure.validate(ngModel[field])), | ||||
|                         'picker-icon': structure.format === 'utc' || !structure.format | ||||
|                      }"> | ||||
|     </input><a class="ui-symbol icon icon-calendar" | ||||
|        ng-if="structure.format === 'utc' || !structure.format" | ||||
|        ng-click="picker.active = !picker.active"> | ||||
|     </a> | ||||
|     </input> | ||||
|     <a class="ui-symbol icon icon-calendar" | ||||
|        ng-if="!picker.active && (structure.format === 'utc' || !structure.format)" | ||||
|        ng-click="picker.active = !picker.active"></a> | ||||
|     <!-- If picker active show icon with no onclick to prevent double registration of clicks --> | ||||
|     <a class="ui-symbol icon icon-calendar" ng-if="picker.active"></a> | ||||
|     <mct-popup ng-if="picker.active"> | ||||
|         <div mct-click-elsewhere="picker.active = false"> | ||||
|             <mct-control key="'datetime-picker'" | ||||
|   | ||||
| @@ -72,6 +72,17 @@ define( | ||||
|                     if ($scope.ngBlur) { | ||||
|                         $scope.ngBlur(); | ||||
|                     } | ||||
|  | ||||
|                     // If picker is active, dismiss it when valid value has been selected | ||||
|                     // This 'if' is to avoid unnecessary validation if picker is not active | ||||
|                     if ($scope.picker.active) { | ||||
|                         if ($scope.structure.validate && $scope.structure.validate($scope.ngModel[$scope.field])) { | ||||
|                             $scope.picker.active = false; | ||||
|                         } else if (!$scope.structure.validate) { | ||||
|                             //If picker visible, but no validation function, hide picker | ||||
|                             $scope.picker.active = false; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -93,7 +104,6 @@ define( | ||||
|             $scope.$watch('ngModel[field]', updateFromModel); | ||||
|             $scope.$watch('pickerModel.value', updateFromPicker); | ||||
|             $scope.$watch('textValue', updateFromView); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         return DateTimeFieldController; | ||||
|   | ||||
| @@ -51,7 +51,9 @@ define( | ||||
|                         yMax = yMin + rect.height; | ||||
|  | ||||
|                     if (x < xMin || x > xMax || y < yMin || y > yMax) { | ||||
|                         scope.$eval(attrs.mctClickElsewhere); | ||||
|                         scope.$apply(function () { | ||||
|                             scope.$eval(attrs.mctClickElsewhere); | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -104,6 +104,8 @@ define( | ||||
|                 }); | ||||
|  | ||||
|                 it("triggers an evaluation of its related Angular expression", function () { | ||||
|                     expect(mockScope.$apply).toHaveBeenCalled(); | ||||
|                     mockScope.$apply.mostRecentCall.args[0](); | ||||
|                     expect(mockScope.$eval) | ||||
|                         .toHaveBeenCalledWith(testAttrs.mctClickElsewhere); | ||||
|                 }); | ||||
|   | ||||
| @@ -14,7 +14,7 @@ $colorAHov: #fff; | ||||
| $contrastRatioPercent: 7%; | ||||
| $hoverRatioPercent: 10%; | ||||
| $basicCr: 3px; | ||||
| $controlCr: 3px; | ||||
| $controlCr: 2px; | ||||
| $smallCr: 2px; | ||||
|  | ||||
| // Buttons and Controls | ||||
| @@ -183,12 +183,13 @@ $scrollbarThumbColorOverlay: lighten($colorOvrBg, 10%); | ||||
| $scrollbarThumbColorOverlayHov: lighten($scrollbarThumbColorOverlay, 2%); | ||||
|  | ||||
| // Splitter | ||||
| $splitterD: 25px; // splitterD and HandleD should both be odd, or even | ||||
| $splitterD: 17px; // splitterD and $splitterHandleD should both be odd, or even | ||||
| $splitterHandleD: 1px; | ||||
| $splitterDSm: 17px; // Smaller splitter, used inside elements like a Timeline view | ||||
| $colorSplitterBg: rgba(#fff, 0.1); //pullForward($colorBodyBg, 5%); | ||||
| $splitterShdw: rgba(black, 0.4) 0 0 3px; | ||||
| $splitterEndCr: none; | ||||
| $colorSplitterHover: pullForward($colorBodyBg, 15%); | ||||
| $colorSplitterHover: pullForward($colorBodyBg, 40%); | ||||
| $colorSplitterActive: $colorKey; | ||||
|  | ||||
| // Mobile | ||||
|   | ||||
| @@ -183,12 +183,12 @@ $scrollbarThumbColorOverlay: darken($colorOvrBg, 50%); | ||||
| $scrollbarThumbColorOverlayHov: $scrollbarThumbColorHov; | ||||
|  | ||||
| // Splitter | ||||
| $splitterD: 24px; | ||||
| $splitterD: 16px; // splitterD and $splitterHandleD should both be odd, or even | ||||
| $splitterHandleD: 2px; | ||||
| $colorSplitterBg: pullForward($colorBodyBg, 10%); | ||||
| $splitterShdw: none; | ||||
| $splitterEndCr: none; | ||||
| $colorSplitterHover: none; | ||||
| $colorSplitterHover: pullForward($colorBodyBg, 30%); | ||||
| $colorSplitterActive: $colorKey; | ||||
|  | ||||
| // Mobile | ||||
|   | ||||
| @@ -60,11 +60,6 @@ define( | ||||
|             this.$q = $q; | ||||
|         } | ||||
|  | ||||
|         function getKey(id) { | ||||
|             var parts = id.split(":"); | ||||
|             return parts.length > 1 ? parts.slice(1).join(":") : id; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Checks if the value returned is falsey, and if so returns a | ||||
|          * rejected promise | ||||
| @@ -131,7 +126,7 @@ define( | ||||
|             // ...and persist | ||||
|             return persistenceFn.apply(persistenceService, [ | ||||
|                 this.getSpace(), | ||||
|                 getKey(domainObject.getId()), | ||||
|                 this.getKey(), | ||||
|                 domainObject.getModel() | ||||
|             ]).then(function (result) { | ||||
|                 return rejectIfFalsey(result, self.$q); | ||||
| @@ -159,7 +154,7 @@ define( | ||||
|  | ||||
|             return this.persistenceService.readObject( | ||||
|                     this.getSpace(), | ||||
|                     this.domainObject.getId() | ||||
|                     this.getKey() | ||||
|                 ).then(updateModel); | ||||
|         }; | ||||
|  | ||||
| @@ -178,6 +173,17 @@ define( | ||||
|             return this.identifierService.parse(id).getSpace(); | ||||
|         }; | ||||
|  | ||||
|  | ||||
|         /** | ||||
|          * Get the key for this domain object in the given space. | ||||
|          * | ||||
|          * @returns {string} the key of the object in it's space. | ||||
|          */ | ||||
|         PersistenceCapability.prototype.getKey = function () { | ||||
|             var id = this.domainObject.getId(); | ||||
|             return this.identifierService.parse(id).getKey(); | ||||
|         }; | ||||
|  | ||||
|         return PersistenceCapability; | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -35,7 +35,8 @@ define( | ||||
|                 mockNofificationService, | ||||
|                 mockCacheService, | ||||
|                 mockQ, | ||||
|                 id = "object id", | ||||
|                 key = "persistence key", | ||||
|                 id = "object identifier", | ||||
|                 model, | ||||
|                 SPACE = "some space", | ||||
|                 persistence, | ||||
| @@ -101,6 +102,7 @@ define( | ||||
|                 }); | ||||
|                 mockIdentifierService.parse.andReturn(mockIdentifier); | ||||
|                 mockIdentifier.getSpace.andReturn(SPACE); | ||||
|                 mockIdentifier.getKey.andReturn(key); | ||||
|                 persistence = new PersistenceCapability( | ||||
|                     mockCacheService, | ||||
|                     mockPersistenceService, | ||||
| @@ -124,7 +126,7 @@ define( | ||||
|  | ||||
|                     expect(mockPersistenceService.createObject).toHaveBeenCalledWith( | ||||
|                         SPACE, | ||||
|                         id, | ||||
|                         key, | ||||
|                         model | ||||
|                     ); | ||||
|                 }); | ||||
| @@ -138,7 +140,7 @@ define( | ||||
|  | ||||
|                     expect(mockPersistenceService.updateObject).toHaveBeenCalledWith( | ||||
|                         SPACE, | ||||
|                         id, | ||||
|                         key, | ||||
|                         model | ||||
|                     ); | ||||
|                 }); | ||||
|   | ||||
| @@ -82,16 +82,6 @@ define( | ||||
|                 expect(result.a.getModel()).toEqual(model); | ||||
|             }); | ||||
|  | ||||
|             //TODO: Disabled for NEM Beta | ||||
|             xit("provides a new, fully constituted domain object for a" + | ||||
|                 " provided model", function () { | ||||
|                 var model = { someKey: "some value"}, | ||||
|                     result; | ||||
|                 result = provider.newObject("a", model); | ||||
|                 expect(result.getId()).toEqual("a"); | ||||
|                 expect(result.getModel()).toEqual(model); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -81,6 +81,7 @@ define( | ||||
|             if (phase.toLowerCase() === 'preparing' && !this.dialog) { | ||||
|                 this.dialog = this.dialogService.showBlockingMessage({ | ||||
|                     title: "Preparing to copy objects", | ||||
|                     hint: "Do not navigate away from this page or close this browser tab while this message is displayed.", | ||||
|                     unknownProgress: true, | ||||
|                     severity: "info" | ||||
|                 }); | ||||
|   | ||||
| @@ -24,10 +24,7 @@ define( | ||||
|     [], | ||||
|     function () { | ||||
|  | ||||
|         var DISALLOWED_ACTIONS = [ | ||||
|             "move", | ||||
|             "copy" | ||||
|         ]; | ||||
|         var DISALLOWED_ACTIONS = ["move"]; | ||||
|  | ||||
|         /** | ||||
|          * This policy prevents performing move/copy/link actions across | ||||
|   | ||||
| @@ -70,27 +70,25 @@ define( | ||||
|                 policy = new CrossSpacePolicy(); | ||||
|             }); | ||||
|  | ||||
|             ['move', 'copy'].forEach(function (key) { | ||||
|                 describe("for " + key + " actions", function () { | ||||
|                     beforeEach(function () { | ||||
|                         testActionMetadata.key = key; | ||||
|                     }); | ||||
|             describe("for move actions", function () { | ||||
|                 beforeEach(function () { | ||||
|                     testActionMetadata.key = 'move'; | ||||
|                 }); | ||||
|  | ||||
|                     it("allows same-space changes", function () { | ||||
|                         expect(policy.allow(mockAction, sameSpaceContext)) | ||||
|                             .toBe(true); | ||||
|                     }); | ||||
|                 it("allows same-space changes", function () { | ||||
|                     expect(policy.allow(mockAction, sameSpaceContext)) | ||||
|                         .toBe(true); | ||||
|                 }); | ||||
|  | ||||
|                     it("disallows cross-space changes", function () { | ||||
|                         expect(policy.allow(mockAction, crossSpaceContext)) | ||||
|                             .toBe(false); | ||||
|                     }); | ||||
|                 it("disallows cross-space changes", function () { | ||||
|                     expect(policy.allow(mockAction, crossSpaceContext)) | ||||
|                         .toBe(false); | ||||
|                 }); | ||||
|  | ||||
|                     it("allows actions with no selectedObject", function () { | ||||
|                         expect(policy.allow(mockAction, { | ||||
|                             domainObject: makeObject('a') | ||||
|                         })).toBe(true); | ||||
|                     }); | ||||
|                 it("allows actions with no selectedObject", function () { | ||||
|                     expect(policy.allow(mockAction, { | ||||
|                         domainObject: makeObject('a') | ||||
|                     })).toBe(true); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|   | ||||
| @@ -42,7 +42,7 @@ define( | ||||
|                 'parent' | ||||
|             ]; | ||||
|  | ||||
|         xdescribe("ConductorRepresenter", function () { | ||||
|         describe("ConductorRepresenter", function () { | ||||
|             var mockThrottle, | ||||
|                 mockConductorService, | ||||
|                 mockCompile, | ||||
|   | ||||
| @@ -1,23 +1,18 @@ | ||||
| <div class="t-imagery" ng-controller="ImageryController as imagery"> | ||||
|     <div | ||||
|         class="l-image-main-wrapper" | ||||
|     <div class="l-image-main-wrapper l-flex-col" | ||||
|         ng-mouseenter="showLocalControls = true;" | ||||
|         ng-mouseleave="showLocalControls = false;" | ||||
|         > | ||||
|         ng-mouseleave="showLocalControls = false;"> | ||||
|         <div | ||||
|             class="l-local-controls s-local-controls" | ||||
|             ng-show="false && showLocalControls" | ||||
|              > | ||||
|             <a | ||||
|                class="s-btn" | ||||
|             ng-show="false && showLocalControls"> | ||||
|             <a class="s-btn" | ||||
|                ng-click="plot.stepBackPanZoom()" | ||||
|                ng-show="1" | ||||
|                title="Restore previous pan/zoom"> | ||||
|                 <span class="ui-symbol icon"><</span> | ||||
|             </a> | ||||
|  | ||||
|             <a | ||||
|                class="s-btn" | ||||
|             <a class="s-btn" | ||||
|                ng-click="plot.unzoom()" | ||||
|                ng-show="1" | ||||
|                title="Reset pan/zoom"> | ||||
| @@ -25,29 +20,23 @@ | ||||
|             </a> | ||||
|         </div> | ||||
|  | ||||
|         <div | ||||
|             class="l-image-main s-image-main" | ||||
|         <div class="l-image-main s-image-main flex-elem grows" | ||||
|             ng-class="{ paused: imagery.paused(), stale:false }" | ||||
|             mct-background-image="imagery.getImageUrl()" | ||||
|             > | ||||
|             mct-background-image="imagery.getImageUrl()"> | ||||
|         </div> | ||||
|  | ||||
|         <div class="l-image-main-controlbar l-flex-row"> | ||||
|         <div class="l-image-main-controlbar flex-elem l-flex-row"> | ||||
|             <div class="left flex-elem grows"> | ||||
|                 <a | ||||
|                     class="s-btn show-thumbs sm hidden" | ||||
|                     ng-click="showThumbsBubble = (showThumbsBubble)? false:true" | ||||
|                     ><span class="ui-symbol icon"></span></a> | ||||
|                 <a class="s-btn show-thumbs sm hidden" | ||||
|                     ng-click="showThumbsBubble = (showThumbsBubble)? false:true"><span class="ui-symbol icon"></span></a> | ||||
|                 <span class="l-timezone">{{imagery.getZone()}}</span> | ||||
|                 <span class="l-time">{{imagery.getTime()}}</span> | ||||
|                 <span class="l-date">{{imagery.getDate()}}</span> | ||||
|             </div> | ||||
|             <div class="right flex-elem"> | ||||
|                 <a | ||||
|                     class="s-btn pause-play" | ||||
|                 <a class="s-btn pause-play" | ||||
|                     ng-click="imagery.paused(!imagery.paused())" | ||||
|                     ng-class="{ paused: imagery.paused() }" | ||||
|                     ><span class="ui-symbol icon"></span></a> | ||||
|                     ng-class="{ paused: imagery.paused() }"><span class="ui-symbol icon"></span></a> | ||||
|                 <a href="{{imagery.getImageUrl()}}" | ||||
|                    ng-if="imagery.getImageUrl()" | ||||
|                    target="_blank" | ||||
| @@ -58,8 +47,7 @@ | ||||
|                     class="s-btn l-mag s-mag ui-symbol vsm" | ||||
|                     ng-click="clipped = false" | ||||
|                     ng-show="clipped === true" | ||||
|                     title="Not all of image is visible; click to reset." | ||||
|                     ></a> | ||||
|                     title="Not all of image is visible; click to reset."></a> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|   | ||||
| @@ -109,7 +109,7 @@ define([ | ||||
|                 { | ||||
|                     "key": "HistoricalTableController", | ||||
|                     "implementation": HistoricalTableController, | ||||
|                     "depends": ["$scope", "telemetryHandler", "telemetryFormatter"] | ||||
|                     "depends": ["$scope", "telemetryHandler", "telemetryFormatter", "$timeout"] | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "RealtimeTableController", | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| <div ng-controller="HistoricalTableController"> | ||||
| <div ng-controller="HistoricalTableController" ng-class="{'loading': loading}"> | ||||
|     <mct-table | ||||
|         headers="headers" | ||||
|         rows="rows" | ||||
|         enableFilter="true" | ||||
|         enableSort="true"> | ||||
|         enableSort="true" | ||||
|         class="tabular-holder t-exportable"> | ||||
|     </mct-table> | ||||
| </div> | ||||
| @@ -1,4 +1,9 @@ | ||||
| <div class="l-view-section scrolling" style="overflow: auto;"> | ||||
| <a class="t-btn l-btn s-btn t-export" | ||||
|    ng-click="exportAsCSV()" | ||||
|    title="Export This View's Data"> | ||||
|     Export | ||||
| </a> | ||||
| <div class="l-view-section scrolling" style="overflow: auto;" mct-resize="resize()"> | ||||
|     <table class="sizing-table"> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
|         rows="rows" | ||||
|         enableFilter="true" | ||||
|         enableSort="true" | ||||
|         auto-scroll="autoScroll"> | ||||
|         class="tabular-holder t-exportable" | ||||
|         auto-scroll="true"> | ||||
|     </mct-table> | ||||
| </div> | ||||
| @@ -25,6 +25,7 @@ define( | ||||
|         './TelemetryTableController' | ||||
|     ], | ||||
|     function (TableController) { | ||||
|         var BATCH_SIZE = 1000; | ||||
|  | ||||
|         /** | ||||
|          * Extends TelemetryTableController and adds real-time streaming | ||||
| @@ -35,32 +36,82 @@ define( | ||||
|          * @param telemetryFormatter | ||||
|          * @constructor | ||||
|          */ | ||||
|         function HistoricalTableController($scope, telemetryHandler, telemetryFormatter) { | ||||
|         function HistoricalTableController($scope, telemetryHandler, telemetryFormatter, $timeout) { | ||||
|             var self = this; | ||||
|  | ||||
|             this.$timeout = $timeout; | ||||
|             this.timeoutHandle = undefined; | ||||
|             this.batchSize = BATCH_SIZE; | ||||
|  | ||||
|             $scope.$on("$destroy", function () { | ||||
|                 if (self.timeoutHandle) { | ||||
|                     self.$timeout.cancel(self.timeoutHandle); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             TableController.call(this, $scope, telemetryHandler, telemetryFormatter); | ||||
|         } | ||||
|  | ||||
|         HistoricalTableController.prototype = Object.create(TableController.prototype); | ||||
|  | ||||
|         /** | ||||
|          * Populates historical data on scope when it becomes available from | ||||
|          * the telemetry API | ||||
|          * Set provided row data on scope, and cancel loading spinner | ||||
|          * @private | ||||
|          */ | ||||
|         HistoricalTableController.prototype.addHistoricalData = function () { | ||||
|             var rowData = [], | ||||
|                 self = this; | ||||
|  | ||||
|             this.handle.getTelemetryObjects().forEach(function (telemetryObject) { | ||||
|                 var series = self.handle.getSeries(telemetryObject) || {}, | ||||
|                     pointCount = series.getPointCount ? series.getPointCount() : 0, | ||||
|                     i = 0; | ||||
|  | ||||
|                 for (; i < pointCount; i++) { | ||||
|                     rowData.push(self.table.getRowValues(telemetryObject, | ||||
|                         self.handle.makeDatum(telemetryObject, series, i))); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|         HistoricalTableController.prototype.doneProcessing = function (rowData) { | ||||
|             this.$scope.rows = rowData; | ||||
|             this.$scope.loading = false; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Processes an array of objects, formatting the telemetry available | ||||
|          * for them and setting it on scope when done | ||||
|          * @private | ||||
|          */ | ||||
|         HistoricalTableController.prototype.processTelemetryObjects = function (objects, offset, start, rowData) { | ||||
|             var telemetryObject = objects[offset], | ||||
|                 series, | ||||
|                 i = start, | ||||
|                 pointCount, | ||||
|                 end; | ||||
|  | ||||
|             //No more objects to process | ||||
|             if (!telemetryObject) { | ||||
|                 return this.doneProcessing(rowData); | ||||
|             } | ||||
|  | ||||
|             series = this.handle.getSeries(telemetryObject); | ||||
|  | ||||
|             pointCount = series.getPointCount(); | ||||
|             end = Math.min(start + this.batchSize, pointCount); | ||||
|  | ||||
|             //Process rows in a batch with size not exceeding a maximum length | ||||
|             for (; i < end; i++) { | ||||
|                 rowData.push(this.table.getRowValues(telemetryObject, | ||||
|                     this.handle.makeDatum(telemetryObject, series, i))); | ||||
|             } | ||||
|  | ||||
|             //Done processing all rows for this object. | ||||
|             if (end >= pointCount) { | ||||
|                 offset++; | ||||
|                 end = 0; | ||||
|             } | ||||
|  | ||||
|             // Done processing either a batch or an object, yield process | ||||
|             // before continuing processing | ||||
|             this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, objects, offset, end, rowData)); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|         * Populates historical data on scope when it becomes available from | ||||
|         * the telemetry API | ||||
|         */ | ||||
|         HistoricalTableController.prototype.addHistoricalData = function () { | ||||
|             if (this.timeoutHandle) { | ||||
|                 this.$timeout.cancel(this.timeoutHandle); | ||||
|             } | ||||
|  | ||||
|             this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, this.handle.getTelemetryObjects(), 0, 0, [])); | ||||
|         }; | ||||
|  | ||||
|         return HistoricalTableController; | ||||
|   | ||||
| @@ -12,7 +12,7 @@ define( | ||||
|          * @param element | ||||
|          * @constructor | ||||
|          */ | ||||
|         function MCTTableController($scope, $timeout, element) { | ||||
|         function MCTTableController($scope, $timeout, element, exportService) { | ||||
|             var self = this; | ||||
|  | ||||
|             this.$scope = $scope; | ||||
| @@ -46,6 +46,16 @@ define( | ||||
|  | ||||
|             setDefaults($scope); | ||||
|  | ||||
|             $scope.exportAsCSV = function () { | ||||
|                 var headers = $scope.displayHeaders; | ||||
|                 exportService.exportCSV($scope.displayRows.map(function (row) { | ||||
|                     return headers.reduce(function (r, header) { | ||||
|                         r[header] = row[header].text; | ||||
|                         return r; | ||||
|                     }, {}); | ||||
|                 }), { headers: headers }); | ||||
|             }; | ||||
|  | ||||
|             $scope.toggleSort = function (key) { | ||||
|                 if (!$scope.enableSort) { | ||||
|                     return; | ||||
| @@ -76,6 +86,12 @@ define( | ||||
|              */ | ||||
|             $scope.$on('add:row', this.addRow.bind(this)); | ||||
|             $scope.$on('remove:row', this.removeRow.bind(this)); | ||||
|  | ||||
|             /* | ||||
|              * Listen for resize events to trigger recalculation of table width | ||||
|              */ | ||||
|             $scope.resize = this.setElementSizes.bind(this); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -38,30 +38,7 @@ define( | ||||
|         function RealtimeTableController($scope, telemetryHandler, telemetryFormatter) { | ||||
|             TableController.call(this, $scope, telemetryHandler, telemetryFormatter); | ||||
|  | ||||
|             $scope.autoScroll = false; | ||||
|             this.maxRows = 100000; | ||||
|  | ||||
|             /* | ||||
|              * Determine if auto-scroll should be enabled. Is enabled | ||||
|              * automatically when telemetry type is string | ||||
|              */ | ||||
|             function hasStringTelemetry(domainObject) { | ||||
|                 var telemetry = domainObject && | ||||
|                         domainObject.getCapability('telemetry'), | ||||
|                     metadata = telemetry ? telemetry.getMetadata() : {}, | ||||
|                     ranges = metadata.ranges || []; | ||||
|  | ||||
|                 return ranges.some(function (range) { | ||||
|                     return range.format === 'string'; | ||||
|                 }); | ||||
|             } | ||||
|             $scope.$watch('domainObject', function (domainObject) { | ||||
|                 //When a domain object becomes available, check whether the | ||||
|                 // view should auto-scroll to the bottom. | ||||
|                 if (domainObject && hasStringTelemetry(domainObject)) { | ||||
|                     $scope.autoScroll = true; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         RealtimeTableController.prototype = Object.create(TableController.prototype); | ||||
| @@ -91,6 +68,7 @@ define( | ||||
|                         self.$scope.rows.length - 1); | ||||
|                 } | ||||
|             }); | ||||
|             this.$scope.loading = false; | ||||
|         }; | ||||
|  | ||||
|         return RealtimeTableController; | ||||
|   | ||||
| @@ -72,10 +72,10 @@ define( | ||||
|              * Maintain a configuration object on scope that stores column | ||||
|              * configuration. On change, synchronize with object model. | ||||
|              */ | ||||
|             $scope.$watchCollection('configuration.table.columns', function (columns) { | ||||
|                 if (columns) { | ||||
|             $scope.$watchCollection('configuration.table.columns', function (newColumns, oldColumns) { | ||||
|                 if (newColumns !== oldColumns) { | ||||
|                     self.domainObject.useCapability('mutation', function (model) { | ||||
|                         model.configuration.table.columns = columns; | ||||
|                         model.configuration.table.columns = newColumns; | ||||
|                     }); | ||||
|                     self.domainObject.getCapability('persistence').persist(); | ||||
|                 } | ||||
|   | ||||
| @@ -83,16 +83,24 @@ define( | ||||
|          * @private | ||||
|          */ | ||||
|         TelemetryTableController.prototype.registerChangeListeners = function () { | ||||
|             var self = this; | ||||
|             this.unregisterChangeListeners(); | ||||
|  | ||||
|             // When composition changes, re-subscribe to the various | ||||
|             // telemetry subscriptions | ||||
|             this.changeListeners.push(this.$scope.$watchCollection( | ||||
|                 'domainObject.getModel().composition', this.subscribe.bind(this))); | ||||
|                 'domainObject.getModel().composition', | ||||
|                 function (newVal, oldVal) { | ||||
|                     if (newVal !== oldVal) { | ||||
|                         self.subscribe(); | ||||
|                     } | ||||
|                 }) | ||||
|             ); | ||||
|  | ||||
|             //Change of bounds in time conductor | ||||
|             this.changeListeners.push(this.$scope.$on('telemetry:display:bounds', | ||||
|                 this.subscribe.bind(this))); | ||||
|                 this.subscribe.bind(this)) | ||||
|             ); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
| @@ -132,6 +140,7 @@ define( | ||||
|             if (this.handle) { | ||||
|                 this.handle.unsubscribe(); | ||||
|             } | ||||
|             this.$scope.loading = true; | ||||
|  | ||||
|             this.handle = this.$scope.domainObject && this.telemetryHandler.handle( | ||||
|                     this.$scope.domainObject, | ||||
|   | ||||
| @@ -81,7 +81,13 @@ define( | ||||
|             return { | ||||
|                 restrict: "E", | ||||
|                 template: TableTemplate, | ||||
|                 controller: ['$scope', '$timeout', '$element', MCTTableController], | ||||
|                 controller: [ | ||||
|                     '$scope', | ||||
|                     '$timeout', | ||||
|                     '$element', | ||||
|                     'exportService', | ||||
|                     MCTTableController | ||||
|                 ], | ||||
|                 scope: { | ||||
|                     headers: "=", | ||||
|                     rows: "=", | ||||
|   | ||||
| @@ -30,23 +30,21 @@ define( | ||||
|         var TEST_DOMAIN_VALUE = "some formatted domain value"; | ||||
|  | ||||
|         describe("A domain column", function () { | ||||
|             var mockDataSet, | ||||
|             var mockDatum, | ||||
|                 testMetadata, | ||||
|                 mockFormatter, | ||||
|                 column; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockDataSet = jasmine.createSpyObj( | ||||
|                     "data", | ||||
|                     ["getDomainValue"] | ||||
|                 ); | ||||
|  | ||||
|                 mockFormatter = jasmine.createSpyObj( | ||||
|                     "formatter", | ||||
|                     ["formatDomainValue", "formatRangeValue"] | ||||
|                 ); | ||||
|                 testMetadata = { | ||||
|                     key: "testKey", | ||||
|                     name: "Test Name" | ||||
|                     name: "Test Name", | ||||
|                     format: "Test Format" | ||||
|                 }; | ||||
|                 mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE); | ||||
|  | ||||
| @@ -57,24 +55,24 @@ define( | ||||
|                 expect(column.getTitle()).toEqual("Test Name"); | ||||
|             }); | ||||
|  | ||||
|             xit("looks up data from a data set", function () { | ||||
|                 column.getValue(undefined, mockDataSet, 42); | ||||
|                 expect(mockDataSet.getDomainValue) | ||||
|                     .toHaveBeenCalledWith(42, "testKey"); | ||||
|             }); | ||||
|             describe("when given a datum", function () { | ||||
|                 beforeEach(function () { | ||||
|                     mockDatum = { | ||||
|                         testKey: "testKeyValue" | ||||
|                     }; | ||||
|                 }); | ||||
|  | ||||
|             xit("formats domain values as time", function () { | ||||
|                 mockDataSet.getDomainValue.andReturn(402513731000); | ||||
|                 it("looks up data from the given datum", function () { | ||||
|                     expect(column.getValue(undefined, mockDatum)) | ||||
|                         .toEqual({ text: TEST_DOMAIN_VALUE }); | ||||
|                 }); | ||||
|  | ||||
|                 // Should have just given the value the formatter gave | ||||
|                 expect(column.getValue(undefined, mockDataSet, 42).text) | ||||
|                     .toEqual(TEST_DOMAIN_VALUE); | ||||
|                 it("uses formatter to format domain values as requested", function () { | ||||
|                     column.getValue(undefined, mockDatum); | ||||
|                     expect(mockFormatter.formatDomainValue) | ||||
|                         .toHaveBeenCalledWith("testKeyValue", "Test Format"); | ||||
|                 }); | ||||
|  | ||||
|                 // Make sure that service interactions were as expected | ||||
|                 expect(mockFormatter.formatDomainValue) | ||||
|                     .toHaveBeenCalledWith(402513731000); | ||||
|                 expect(mockFormatter.formatRangeValue) | ||||
|                     .not.toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|   | ||||
| @@ -34,6 +34,8 @@ define( | ||||
|                 mockDomainObject, | ||||
|                 mockTable, | ||||
|                 mockConfiguration, | ||||
|                 mockAngularTimeout, | ||||
|                 mockTimeoutHandle, | ||||
|                 watches, | ||||
|                 controller; | ||||
|  | ||||
| @@ -63,6 +65,11 @@ define( | ||||
|                     watches[expression] = callback; | ||||
|                 }); | ||||
|  | ||||
|                 mockTimeoutHandle = jasmine.createSpy("timeoutHandle"); | ||||
|                 mockAngularTimeout = jasmine.createSpy("$timeout"); | ||||
|                 mockAngularTimeout.andReturn(mockTimeoutHandle); | ||||
|                 mockAngularTimeout.cancel = jasmine.createSpy("cancelTimeout"); | ||||
|  | ||||
|                 mockConfiguration = { | ||||
|                     'range1': true, | ||||
|                     'range2': true, | ||||
| @@ -107,7 +114,7 @@ define( | ||||
|                 ]); | ||||
|                 mockTelemetryHandler.handle.andReturn(mockTelemetryHandle); | ||||
|  | ||||
|                 controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter); | ||||
|                 controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter, mockAngularTimeout); | ||||
|                 controller.table = mockTable; | ||||
|                 controller.handle = mockTelemetryHandle; | ||||
|             }); | ||||
| @@ -163,6 +170,13 @@ define( | ||||
|  | ||||
|                 controller.addHistoricalData(mockDomainObject, mockSeries); | ||||
|  | ||||
|                 // Angular timeout is called a minumum of twice, regardless | ||||
|                 // of batch size used. | ||||
|                 expect(mockAngularTimeout).toHaveBeenCalled(); | ||||
|                 mockAngularTimeout.mostRecentCall.args[0](); | ||||
|                 expect(mockAngularTimeout.calls.length).toEqual(2); | ||||
|                 mockAngularTimeout.mostRecentCall.args[0](); | ||||
|  | ||||
|                 expect(controller.$scope.rows.length).toBe(5); | ||||
|                 expect(controller.$scope.rows[0]).toBe(mockRow); | ||||
|             }); | ||||
| @@ -198,7 +212,7 @@ define( | ||||
|                     ' object composition changes', function () { | ||||
|                     controller.registerChangeListeners(); | ||||
|                     expect(watches['domainObject.getModel().composition']).toBeDefined(); | ||||
|                     watches['domainObject.getModel().composition'](); | ||||
|                     watches['domainObject.getModel().composition']([], []); | ||||
|                     expect(controller.subscribe).toHaveBeenCalled(); | ||||
|                 }); | ||||
|  | ||||
| @@ -219,6 +233,78 @@ define( | ||||
|                 }); | ||||
|  | ||||
|             }); | ||||
|             describe('Yields thread', function () { | ||||
|                 var mockSeries, | ||||
|                     mockRow; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     mockSeries = { | ||||
|                         getPointCount: function () { | ||||
|                             return 5; | ||||
|                         }, | ||||
|                         getDomainValue: function () { | ||||
|                             return 'Domain Value'; | ||||
|                         }, | ||||
|                         getRangeValue: function () { | ||||
|                             return 'Range Value'; | ||||
|                         } | ||||
|                     }; | ||||
|                     mockRow = {'domain': 'Domain Value', 'range': 'Range Value'}; | ||||
|  | ||||
|                     mockTelemetryHandle.makeDatum.andCallFake(function () { | ||||
|                         return mockRow; | ||||
|                     }); | ||||
|                     mockTable.getRowValues.andReturn(mockRow); | ||||
|                     mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]); | ||||
|                     mockTelemetryHandle.getSeries.andReturn(mockSeries); | ||||
|                 }); | ||||
|                 it('when row count exceeds batch size', function () { | ||||
|                     controller.batchSize = 3; | ||||
|                     controller.addHistoricalData(mockDomainObject, mockSeries); | ||||
|  | ||||
|                     //Timeout is called a minimum of two times | ||||
|                     expect(mockAngularTimeout).toHaveBeenCalled(); | ||||
|                     mockAngularTimeout.mostRecentCall.args[0](); | ||||
|  | ||||
|                     expect(mockAngularTimeout.calls.length).toEqual(2); | ||||
|                     mockAngularTimeout.mostRecentCall.args[0](); | ||||
|  | ||||
|                     //Because it yields, timeout will have been called a | ||||
|                     // third time for the batch. | ||||
|                     expect(mockAngularTimeout.calls.length).toEqual(3); | ||||
|                     mockAngularTimeout.mostRecentCall.args[0](); | ||||
|  | ||||
|                     expect(controller.$scope.rows.length).toBe(5); | ||||
|                     expect(controller.$scope.rows[0]).toBe(mockRow); | ||||
|                 }); | ||||
|                 it('cancelling any outstanding timeouts', function () { | ||||
|                     controller.batchSize = 3; | ||||
|                     controller.addHistoricalData(mockDomainObject, mockSeries); | ||||
|  | ||||
|                     expect(mockAngularTimeout).toHaveBeenCalled(); | ||||
|                     mockAngularTimeout.mostRecentCall.args[0](); | ||||
|  | ||||
|                     controller.addHistoricalData(mockDomainObject, mockSeries); | ||||
|  | ||||
|                     expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle); | ||||
|                 }); | ||||
|                 it('cancels timeout on scope destruction', function () { | ||||
|                     controller.batchSize = 3; | ||||
|                     controller.addHistoricalData(mockDomainObject, mockSeries); | ||||
|  | ||||
|                     //Destroy is used by parent class as well, so multiple | ||||
|                     // calls are made to scope.$on | ||||
|                     var destroyCalls = mockScope.$on.calls.filter(function (call) { | ||||
|                         return call.args[0] === '$destroy'; | ||||
|                     }); | ||||
|                     //Call destroy function | ||||
|                     expect(destroyCalls.length).toEqual(2); | ||||
|  | ||||
|                     destroyCalls[0].args[1](); | ||||
|                     expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle); | ||||
|  | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -32,7 +32,8 @@ define( | ||||
|                 mockScope, | ||||
|                 watches, | ||||
|                 mockTimeout, | ||||
|                 mockElement; | ||||
|                 mockElement, | ||||
|                 mockExportService; | ||||
|  | ||||
|             function promise(value) { | ||||
|                 return { | ||||
| @@ -67,11 +68,20 @@ define( | ||||
|                     offsetHeight: 1000 | ||||
|                 }; | ||||
|  | ||||
|                 mockExportService = jasmine.createSpyObj('exportService', [ | ||||
|                     'exportCSV' | ||||
|                 ]); | ||||
|  | ||||
|                 mockScope.displayHeaders = true; | ||||
|                 mockTimeout = jasmine.createSpy('$timeout'); | ||||
|                 mockTimeout.andReturn(promise(undefined)); | ||||
|  | ||||
|                 controller = new MCTTableController(mockScope, mockTimeout, mockElement); | ||||
|                 controller = new MCTTableController( | ||||
|                     mockScope, | ||||
|                     mockTimeout, | ||||
|                     mockElement, | ||||
|                     mockExportService | ||||
|                 ); | ||||
|                 spyOn(controller, 'setVisibleRows').andCallThrough(); | ||||
|             }); | ||||
|  | ||||
| @@ -149,6 +159,22 @@ define( | ||||
|                     expect(controller.setVisibleRows).toHaveBeenCalled(); | ||||
|                 }); | ||||
|  | ||||
|                 it("can be exported as CSV", function () { | ||||
|                     controller.setRows(testRows); | ||||
|                     controller.setHeaders(Object.keys(testRows[0])); | ||||
|                     mockScope.exportAsCSV(); | ||||
|                     expect(mockExportService.exportCSV) | ||||
|                         .toHaveBeenCalled(); | ||||
|                     mockExportService.exportCSV.mostRecentCall.args[0] | ||||
|                         .forEach(function (row, i) { | ||||
|                             Object.keys(row).forEach(function (k) { | ||||
|                                 expect(row[k]).toEqual( | ||||
|                                     mockScope.displayRows[i][k].text | ||||
|                                 ); | ||||
|                             }); | ||||
|                         }); | ||||
|                 }); | ||||
|  | ||||
|                 describe('sorting', function () { | ||||
|                     var sortedRows; | ||||
|  | ||||
|   | ||||
| @@ -155,13 +155,6 @@ define( | ||||
|                     expect(mockScope.rows[0].row).toBe(1); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it('enables autoscroll for event telemetry', function () { | ||||
|                 controller.subscribe(); | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|                 expect(mockScope.autoScroll).toBe(true); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -91,7 +91,12 @@ define([ | ||||
|                     "name": "Export Timeline as CSV", | ||||
|                     "category": "contextual", | ||||
|                     "implementation": ExportTimelineAsCSVAction, | ||||
|                     "depends": ["exportService", "notificationService"] | ||||
|                     "depends": [ | ||||
|                         "$log", | ||||
|                         "exportService", | ||||
|                         "notificationService", | ||||
|                         "resources[]" | ||||
|                     ] | ||||
|                 } | ||||
|             ], | ||||
|             "constants": [ | ||||
| @@ -467,6 +472,7 @@ define([ | ||||
|                     "implementation": TimelineZoomController, | ||||
|                     "depends": [ | ||||
|                         "$scope", | ||||
|                         "$window", | ||||
|                         "TIMELINE_ZOOM_CONFIGURATION" | ||||
|                     ] | ||||
|                 }, | ||||
|   | ||||
| @@ -23,6 +23,13 @@ | ||||
| .l-timeline-holder { | ||||
|     @include absPosDefault(); | ||||
|  | ||||
|     &.split-layout { | ||||
|         >.splitter { | ||||
|             // Top of splitter within Timelines should be 0 | ||||
|             top: 0; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .l-header { | ||||
|         @include user-select(none); | ||||
|         cursor: default; | ||||
| @@ -58,7 +65,7 @@ | ||||
|                 } | ||||
|                 &.l-tabular-r { | ||||
|                     // Start, end, duration, activity modes columns | ||||
|                     @include scrollH(); | ||||
|                     @include scrollH(scroll); | ||||
|                     left: $timelineTabularTitleW; | ||||
| 	                .l-width { | ||||
| 		                @include absPosDefault(0, visible); | ||||
| @@ -304,11 +311,6 @@ | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|     .splitter { | ||||
|         // Top of splitter within Timelines should be 0 | ||||
|         top: 0; | ||||
|     } | ||||
|  | ||||
| 	// Ticks | ||||
| 	.l-ticks, | ||||
| 	.l-subticks { | ||||
| @@ -331,4 +333,4 @@ | ||||
|     &:hover { | ||||
|         background-color: $colorItemTreeHoverBg; | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="t-timeline-gantt l-timeline-gantt s-timeline-gantt" | ||||
|      ng-class="{ sm: gantt.width(timespan, parameters.scroll, parameters.toPixels) < 25 }" | ||||
|      ng-class="timespan ? { sm: gantt.width(timespan, parameters.scroll, parameters.toPixels) < 25 } : {}" | ||||
|      title="{{model.name}}" | ||||
|      ng-controller="TimelineGanttController as gantt" | ||||
|      ng-style="timespan ? { | ||||
|   | ||||
| @@ -128,7 +128,7 @@ | ||||
|             <div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x"> | ||||
|                 <mct-include key="'timeline-ticks'" | ||||
|                              parameters="{ | ||||
|                                          fullWidth: timelineController.width(zoomController), | ||||
|                                          fullWidth: zoomController.width(timelineController.end()), | ||||
|                                          start: scroll.x, | ||||
|                                          width: scroll.width, | ||||
|                                          step: zoomController.toPixels(zoomController.zoom()), | ||||
| @@ -141,7 +141,7 @@ | ||||
|                  mct-scroll-x="scroll.x" | ||||
|                  mct-scroll-y="scroll.y"> | ||||
|                 <div class="l-width-control" | ||||
|                      ng-style="{ width: timelineController.width(zoomController) + 'px' }"> | ||||
|                      ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }"> | ||||
|                     <div class="t-swimlane s-swimlane l-swimlane" | ||||
|                          ng-repeat="swimlane in timelineController.swimlanes()" | ||||
|                          ng-class="{ | ||||
| @@ -197,7 +197,7 @@ | ||||
|             <div mct-scroll-x="scroll.x" | ||||
|                  class="t-pane-r-scroll-h-control l-scroll-control s-scroll-control"> | ||||
|                 <div class="l-width-control" | ||||
|                      ng-style="{ width: timelineController.width(zoomController) + 'px' }"> | ||||
|                      ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }"> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|   | ||||
| @@ -27,11 +27,15 @@ define([], function () { | ||||
|      * in a domain object's composition. | ||||
|      * @param {number} index the zero-based index of the composition | ||||
|      *        element associated with this column | ||||
|      * @param idMap an object containing key value pairs, where keys | ||||
|      *        are domain object identifiers and values are whatever | ||||
|      *        should appear in CSV output in their place | ||||
|      * @constructor | ||||
|      * @implements {platform/features/timeline.TimelineCSVColumn} | ||||
|      */ | ||||
|     function CompositionColumn(index) { | ||||
|     function CompositionColumn(index, idMap) { | ||||
|         this.index = index; | ||||
|         this.idMap = idMap; | ||||
|     } | ||||
|  | ||||
|     CompositionColumn.prototype.name = function () { | ||||
| @@ -41,7 +45,9 @@ define([], function () { | ||||
|     CompositionColumn.prototype.value = function (domainObject) { | ||||
|         var model = domainObject.getModel(), | ||||
|             composition = model.composition || []; | ||||
|         return (composition[this.index]) || ""; | ||||
|  | ||||
|         return composition.length > this.index ? | ||||
|             this.idMap[composition[this.index]] : ""; | ||||
|     }; | ||||
|  | ||||
|     return CompositionColumn; | ||||
|   | ||||
| @@ -27,14 +27,23 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) { | ||||
|      * | ||||
|      * @param exportService the service used to perform the CSV export | ||||
|      * @param notificationService the service used to show notifications | ||||
|      * @param {Array} resources an array of `resources` extensions | ||||
|      * @param context the Action's context | ||||
|      * @implements {Action} | ||||
|      * @constructor | ||||
|      * @memberof {platform/features/timeline} | ||||
|      */ | ||||
|     function ExportTimelineAsCSVAction(exportService, notificationService, context) { | ||||
|     function ExportTimelineAsCSVAction( | ||||
|         $log, | ||||
|         exportService, | ||||
|         notificationService, | ||||
|         resources, | ||||
|         context | ||||
|     ) { | ||||
|         this.$log = $log; | ||||
|         this.task = new ExportTimelineAsCSVTask( | ||||
|             exportService, | ||||
|             resources, | ||||
|             context.domainObject | ||||
|         ); | ||||
|         this.notificationService = notificationService; | ||||
| @@ -45,13 +54,15 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) { | ||||
|             notification = notificationService.notify({ | ||||
|                 title: "Exporting CSV", | ||||
|                 unknownProgress: true | ||||
|             }); | ||||
|             }), | ||||
|             $log = this.$log; | ||||
|  | ||||
|         return this.task.run() | ||||
|             .then(function () { | ||||
|                 notification.dismiss(); | ||||
|             }) | ||||
|             .catch(function () { | ||||
|             .catch(function (err) { | ||||
|                 $log.warn(err); | ||||
|                 notification.dismiss(); | ||||
|                 notificationService.error("Error exporting CSV"); | ||||
|             }); | ||||
|   | ||||
| @@ -35,11 +35,13 @@ define([ | ||||
|      * @constructor | ||||
|      * @memberof {platform/features/timeline} | ||||
|      * @param exportService the service used to export as CSV | ||||
|      * @param resources the `resources` extension category | ||||
|      * @param {DomainObject} domainObject the timeline being exported | ||||
|      */ | ||||
|     function ExportTimelineAsCSVTask(exportService, domainObject) { | ||||
|     function ExportTimelineAsCSVTask(exportService, resources, domainObject) { | ||||
|         this.domainObject = domainObject; | ||||
|         this.exportService = exportService; | ||||
|         this.resources = resources; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -50,9 +52,10 @@ define([ | ||||
|      */ | ||||
|     ExportTimelineAsCSVTask.prototype.run = function () { | ||||
|         var exportService = this.exportService; | ||||
|         var resources = this.resources; | ||||
|  | ||||
|         function doExport(objects) { | ||||
|             var exporter = new TimelineColumnizer(objects), | ||||
|             var exporter = new TimelineColumnizer(objects, resources), | ||||
|                 options = { headers: exporter.headers() }; | ||||
|             return exporter.rows().then(function (rows) { | ||||
|                 return exportService.exportCSV(rows, options); | ||||
|   | ||||
| @@ -23,19 +23,23 @@ | ||||
| define([], function () { | ||||
|  | ||||
|     /** | ||||
|      * A column showing domain object identifiers. | ||||
|      * A column showing identifying domain objects. | ||||
|      * @constructor | ||||
|      * @param idMap an object containing key value pairs, where keys | ||||
|      *        are domain object identifiers and values are whatever | ||||
|      *        should appear in CSV output in their place | ||||
|      * @implements {platform/features/timeline.TimelineCSVColumn} | ||||
|      */ | ||||
|     function IdColumn() { | ||||
|     function IdColumn(idMap) { | ||||
|         this.idMap = idMap; | ||||
|     } | ||||
|  | ||||
|     IdColumn.prototype.name = function () { | ||||
|         return "Identifier"; | ||||
|         return "Index"; | ||||
|     }; | ||||
|  | ||||
|     IdColumn.prototype.value = function (domainObject) { | ||||
|         return domainObject.getId(); | ||||
|         return this.idMap[domainObject.getId()]; | ||||
|     }; | ||||
|  | ||||
|     return IdColumn; | ||||
|   | ||||
| @@ -27,10 +27,14 @@ define([], function () { | ||||
|      * @constructor | ||||
|      * @param {number} index the zero-based index of the composition | ||||
|      *        element associated with this column | ||||
|      * @param idMap an object containing key value pairs, where keys | ||||
|      *        are domain object identifiers and values are whatever | ||||
|      *        should appear in CSV output in their place | ||||
|      * @implements {platform/features/timeline.TimelineCSVColumn} | ||||
|      */ | ||||
|     function ModeColumn(index) { | ||||
|     function ModeColumn(index, idMap) { | ||||
|         this.index = index; | ||||
|         this.idMap = idMap; | ||||
|     } | ||||
|  | ||||
|     ModeColumn.prototype.name = function () { | ||||
| @@ -39,8 +43,9 @@ define([], function () { | ||||
|  | ||||
|     ModeColumn.prototype.value = function (domainObject) { | ||||
|         var model = domainObject.getModel(), | ||||
|             composition = (model.relationships || {}).modes || []; | ||||
|         return (composition[this.index]) || ""; | ||||
|             modes = (model.relationships || {}).modes || []; | ||||
|         return modes.length > this.index ? | ||||
|             this.idMap[modes[this.index]] : ""; | ||||
|     }; | ||||
|  | ||||
|     return ModeColumn; | ||||
|   | ||||
| @@ -25,13 +25,15 @@ define([ | ||||
|     "./ModeColumn", | ||||
|     "./CompositionColumn", | ||||
|     "./MetadataColumn", | ||||
|     "./TimespanColumn" | ||||
|     "./TimespanColumn", | ||||
|     "./UtilizationColumn" | ||||
| ], function ( | ||||
|     IdColumn, | ||||
|     ModeColumn, | ||||
|     CompositionColumn, | ||||
|     MetadataColumn, | ||||
|     TimespanColumn | ||||
|     TimespanColumn, | ||||
|     UtilizationColumn | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
| @@ -63,15 +65,17 @@ define([ | ||||
|      * | ||||
|      * @param {DomainObject[]} domainObjects the objects to include | ||||
|      *        in the exported data | ||||
|      * @param {Array} resources an array of `resources` extensions | ||||
|      * @constructor | ||||
|      * @memberof {platform/features/timeline} | ||||
|      */ | ||||
|     function TimelineColumnizer(domainObjects) { | ||||
|     function TimelineColumnizer(domainObjects, resources) { | ||||
|         var maxComposition = 0, | ||||
|             maxRelationships = 0, | ||||
|             columnNames = {}, | ||||
|             columns = [], | ||||
|             foundTimespan = false, | ||||
|             idMap, | ||||
|             i; | ||||
|  | ||||
|         function addMetadataProperty(property) { | ||||
| @@ -82,7 +86,12 @@ define([ | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         columns.push(new IdColumn()); | ||||
|         idMap = domainObjects.reduce(function (map, domainObject, index) { | ||||
|             map[domainObject.getId()] = index + 1; | ||||
|             return map; | ||||
|         }, {}); | ||||
|  | ||||
|         columns.push(new IdColumn(idMap)); | ||||
|  | ||||
|         domainObjects.forEach(function (domainObject) { | ||||
|             var model = domainObject.getModel(), | ||||
| @@ -113,12 +122,16 @@ define([ | ||||
|             columns.push(new TimespanColumn(false)); | ||||
|         } | ||||
|  | ||||
|         resources.forEach(function (resource) { | ||||
|             columns.push(new UtilizationColumn(resource)); | ||||
|         }); | ||||
|  | ||||
|         for (i = 0; i < maxComposition; i += 1) { | ||||
|             columns.push(new CompositionColumn(i)); | ||||
|             columns.push(new CompositionColumn(i, idMap)); | ||||
|         } | ||||
|  | ||||
|         for (i = 0; i < maxRelationships; i += 1) { | ||||
|             columns.push(new ModeColumn(i)); | ||||
|             columns.push(new ModeColumn(i, idMap)); | ||||
|         } | ||||
|  | ||||
|         this.domainObjects = domainObjects; | ||||
|   | ||||
							
								
								
									
										72
									
								
								platform/features/timeline/src/actions/UtilizationColumn.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								platform/features/timeline/src/actions/UtilizationColumn.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2009-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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([], function () { | ||||
|     /** | ||||
|      * A column showing utilization costs associated with activities. | ||||
|      * @constructor | ||||
|      * @param {string} key the key for the particular cost | ||||
|      * @implements {platform/features/timeline.TimelineCSVColumn} | ||||
|      */ | ||||
|     function UtilizationColumn(resource) { | ||||
|         this.resource = resource; | ||||
|     } | ||||
|  | ||||
|     UtilizationColumn.prototype.name = function () { | ||||
|         var units = { | ||||
|             "Kbps": "Kb", | ||||
|             "watts": "watt-seconds" | ||||
|         }[this.resource.units] || "unknown units"; | ||||
|  | ||||
|         return this.resource.name + " (" + units + ")"; | ||||
|     }; | ||||
|  | ||||
|     UtilizationColumn.prototype.value = function (domainObject) { | ||||
|         var resource = this.resource; | ||||
|  | ||||
|         function getCost(utilization) { | ||||
|             var seconds = (utilization.end - utilization.start) / 1000; | ||||
|             return seconds * utilization.value; | ||||
|         } | ||||
|  | ||||
|         function getUtilizationValue(utilizations) { | ||||
|             utilizations = utilizations.filter(function (utilization) { | ||||
|                 return utilization.key === resource.key; | ||||
|             }); | ||||
|  | ||||
|             if (utilizations.length === 0) { | ||||
|                 return ""; | ||||
|             } | ||||
|  | ||||
|             return utilizations.map(getCost).reduce(function (a, b) { | ||||
|                 return a + b; | ||||
|             }, 0); | ||||
|         } | ||||
|  | ||||
|         return domainObject.hasCapability('utilization') ? | ||||
|             domainObject.getCapability('utilization').internal() | ||||
|                 .then(getUtilizationValue) : | ||||
|             ""; | ||||
|     }; | ||||
|  | ||||
|     return UtilizationColumn; | ||||
| }); | ||||
| @@ -193,6 +193,13 @@ define( | ||||
|                  * @returns {Promise.<string[]>} a promise for resource identifiers | ||||
|                  */ | ||||
|                 resources: promiseResourceKeys, | ||||
|                 /** | ||||
|                  * Get the resource utilization associated with this object | ||||
|                  * directly, not including any resource utilization associated | ||||
|                  * with contained objects. | ||||
|                  * @returns {Promise.<Array>} | ||||
|                  */ | ||||
|                 internal: promiseInternalUtilization, | ||||
|                 /** | ||||
|                  * Get the resource utilization associated with this | ||||
|                  * object. Results are not sorted. This requires looking | ||||
|   | ||||
| @@ -79,15 +79,6 @@ define( | ||||
|                 graphPopulator.populate(swimlanePopulator.get()); | ||||
|             } | ||||
|  | ||||
|             // Get pixel width for right pane, using zoom controller | ||||
|             function width(zoomController) { | ||||
|                 var start = swimlanePopulator.start(), | ||||
|                     end = swimlanePopulator.end(); | ||||
|                 return zoomController.toPixels(zoomController.duration( | ||||
|                     Math.max(end - start, MINIMUM_DURATION) | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             // Refresh resource graphs | ||||
|             function refresh() { | ||||
|                 if (graphPopulator) { | ||||
| @@ -121,10 +112,10 @@ define( | ||||
|             // Expose active set of swimlanes | ||||
|             return { | ||||
|                 /** | ||||
|                  * Get the width, in pixels, of the timeline area | ||||
|                  * @returns {number} width, in pixels | ||||
|                  * Get the end of the displayed timeline, in milliseconds. | ||||
|                  * @returns {number} the end of the displayed timeline | ||||
|                  */ | ||||
|                 width: width, | ||||
|                 end: swimlanePopulator.end.bind(swimlanePopulator), | ||||
|                 /** | ||||
|                  * Get the swimlanes which should currently be displayed. | ||||
|                  * @returns {TimelineSwimlane[]} the swimlanes | ||||
|   | ||||
| @@ -22,27 +22,17 @@ | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
|         var PADDING = 0.25; | ||||
|  | ||||
|         /** | ||||
|          * Controls the pan-zoom state of a timeline view. | ||||
|          * @constructor | ||||
|          */ | ||||
|         function TimelineZoomController($scope, ZOOM_CONFIGURATION) { | ||||
|         function TimelineZoomController($scope, $window, ZOOM_CONFIGURATION) { | ||||
|             // Prefer to start with the middle index | ||||
|             var zoomLevels = ZOOM_CONFIGURATION.levels || [1000], | ||||
|                 zoomIndex = Math.floor(zoomLevels.length / 2), | ||||
|                 tickWidth = ZOOM_CONFIGURATION.width || 200, | ||||
|                 bounds = { x: 0, width: tickWidth }, | ||||
|                 duration = 86400000; // Default duration in view | ||||
|  | ||||
|             // Round a duration to a larger value, to ensure space for editing | ||||
|             function roundDuration(value) { | ||||
|                 // Ensure there's always an extra day or so | ||||
|                 var tickCount = bounds.width / tickWidth, | ||||
|                     sz = zoomLevels[zoomLevels.length - 1] * tickCount; | ||||
|                 value *= 1.25; // Add 25% padding to start | ||||
|                 return Math.ceil(value / sz) * sz; | ||||
|             } | ||||
|                 tickWidth = ZOOM_CONFIGURATION.width || 200; | ||||
|  | ||||
|             function toMillis(pixels) { | ||||
|                 return (pixels / tickWidth) * zoomLevels[zoomIndex]; | ||||
| @@ -63,14 +53,21 @@ define( | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             function setScroll(x) { | ||||
|                 $window.requestAnimationFrame(function () { | ||||
|                     $scope.scroll.x = x; | ||||
|                     $scope.$apply(); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             function initializeZoomFromTimespan(timespan) { | ||||
|                 var timelineDuration = timespan.getDuration(); | ||||
|                 zoomIndex = 0; | ||||
|                 while (toMillis(bounds.width) < timelineDuration && | ||||
|                 while (toMillis($scope.scroll.width) < timelineDuration && | ||||
|                         zoomIndex < zoomLevels.length - 1) { | ||||
|                     zoomIndex += 1; | ||||
|                 } | ||||
|                 bounds.x = toPixels(timespan.getStart()); | ||||
|                 setScroll(toPixels(timespan.getStart())); | ||||
|             } | ||||
|  | ||||
|             function initializeZoom() { | ||||
| @@ -80,9 +77,6 @@ define( | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             $scope.$watch("scroll", function (scroll) { | ||||
|                 bounds = scroll; | ||||
|             }); | ||||
|             $scope.$watch("domainObject", initializeZoom); | ||||
|  | ||||
|             return { | ||||
| @@ -100,9 +94,10 @@ define( | ||||
|                 zoom: function (amount) { | ||||
|                     // Update the zoom level if called with an argument | ||||
|                     if (arguments.length > 0 && !isNaN(amount)) { | ||||
|                         var bounds = $scope.scroll; | ||||
|                         var center = this.toMillis(bounds.x + bounds.width / 2); | ||||
|                         setZoomLevel(zoomIndex + amount); | ||||
|                         bounds.x = this.toPixels(center) - bounds.width / 2; | ||||
|                         setScroll(this.toPixels(center) - bounds.width / 2); | ||||
|                     } | ||||
|                     return zoomLevels[zoomIndex]; | ||||
|                 }, | ||||
| @@ -124,16 +119,14 @@ define( | ||||
|                  */ | ||||
|                 toMillis: toMillis, | ||||
|                 /** | ||||
|                  * Get or set the current displayed duration. If used as a | ||||
|                  * setter, this will typically be rounded up to ensure extra | ||||
|                  * space is available at the right. | ||||
|                  * @returns {number} duration, in milliseconds | ||||
|                  * Get the pixel width necessary to fit the specified | ||||
|                  * timestamp, expressed as an offset in milliseconds from | ||||
|                  * the start of the timeline. | ||||
|                  * @param {number} timestamp the time to display | ||||
|                  */ | ||||
|                 duration: function (value) { | ||||
|                     if (arguments.length > 0) { | ||||
|                         duration = roundDuration(value); | ||||
|                     } | ||||
|                     return duration; | ||||
|                 width: function (timestamp) { | ||||
|                     var pixels = Math.ceil(toPixels(timestamp * (1 + PADDING))); | ||||
|                     return Math.max($scope.scroll.width, pixels); | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|   | ||||
| @@ -23,13 +23,20 @@ | ||||
| define( | ||||
|     ['../../src/actions/CompositionColumn'], | ||||
|     function (CompositionColumn) { | ||||
|         var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f']; | ||||
|  | ||||
|         describe("CompositionColumn", function () { | ||||
|             var testIndex, | ||||
|                 testIdMap, | ||||
|                 column; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testIndex = 3; | ||||
|                 column = new CompositionColumn(testIndex); | ||||
|                 testIdMap = TEST_IDS.reduce(function (map, id, index) { | ||||
|                     map[id] = index; | ||||
|                     return map; | ||||
|                 }, {}); | ||||
|                 column = new CompositionColumn(testIndex, testIdMap); | ||||
|             }); | ||||
|  | ||||
|             it("includes a one-based index in its name", function () { | ||||
| @@ -46,15 +53,13 @@ define( | ||||
|                         'domainObject', | ||||
|                         ['getId', 'getModel', 'getCapability'] | ||||
|                     ); | ||||
|                     testModel = { | ||||
|                         composition: ['a', 'b', 'c', 'd', 'e', 'f'] | ||||
|                     }; | ||||
|                     testModel = { composition: TEST_IDS }; | ||||
|                     mockDomainObject.getModel.andReturn(testModel); | ||||
|                 }); | ||||
|  | ||||
|                 it("returns a corresponding identifier", function () { | ||||
|                 it("returns a corresponding value from the map", function () { | ||||
|                     expect(column.value(mockDomainObject)) | ||||
|                         .toEqual(testModel.composition[testIndex]); | ||||
|                         .toEqual(testIdMap[testModel.composition[testIndex]]); | ||||
|                 }); | ||||
|  | ||||
|                 it("returns nothing when composition is exceeded", function () { | ||||
|   | ||||
| @@ -24,7 +24,8 @@ define( | ||||
|     ['../../src/actions/ExportTimelineAsCSVAction'], | ||||
|     function (ExportTimelineAsCSVAction) { | ||||
|         describe("ExportTimelineAsCSVAction", function () { | ||||
|             var mockExportService, | ||||
|             var mockLog, | ||||
|                 mockExportService, | ||||
|                 mockNotificationService, | ||||
|                 mockNotification, | ||||
|                 mockDomainObject, | ||||
| @@ -39,6 +40,13 @@ define( | ||||
|                     ['getId', 'getModel', 'getCapability', 'hasCapability'] | ||||
|                 ); | ||||
|                 mockType = jasmine.createSpyObj('type', ['instanceOf']); | ||||
|  | ||||
|                 mockLog = jasmine.createSpyObj('$log', [ | ||||
|                     'warn', | ||||
|                     'error', | ||||
|                     'info', | ||||
|                     'debug' | ||||
|                 ]); | ||||
|                 mockExportService = jasmine.createSpyObj( | ||||
|                     'exportService', | ||||
|                     ['exportCSV'] | ||||
| @@ -63,8 +71,10 @@ define( | ||||
|                 testContext = { domainObject: mockDomainObject }; | ||||
|  | ||||
|                 action = new ExportTimelineAsCSVAction( | ||||
|                     mockLog, | ||||
|                     mockExportService, | ||||
|                     mockNotificationService, | ||||
|                     [], | ||||
|                     testContext | ||||
|                 ); | ||||
|             }); | ||||
| @@ -129,8 +139,11 @@ define( | ||||
|                 }); | ||||
|  | ||||
|                 describe("and an error occurs", function () { | ||||
|                     var testError; | ||||
|  | ||||
|                     beforeEach(function () { | ||||
|                         testPromise.reject(); | ||||
|                         testError = { someProperty: "some value" }; | ||||
|                         testPromise.reject(testError); | ||||
|                         waitsFor(function () { | ||||
|                             return mockCallback.calls.length > 0; | ||||
|                         }); | ||||
| @@ -145,6 +158,10 @@ define( | ||||
|                         expect(mockNotificationService.error) | ||||
|                             .toHaveBeenCalledWith(jasmine.any(String)); | ||||
|                     }); | ||||
|  | ||||
|                     it("logs the root cause", function () { | ||||
|                         expect(mockLog.warn).toHaveBeenCalledWith(testError); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|   | ||||
| @@ -52,6 +52,7 @@ define( | ||||
|  | ||||
|                 task = new ExportTimelineAsCSVTask( | ||||
|                     mockExportService, | ||||
|                     [], | ||||
|                     mockDomainObject | ||||
|                 ); | ||||
|             }); | ||||
|   | ||||
| @@ -24,10 +24,12 @@ define( | ||||
|     ['../../src/actions/IdColumn'], | ||||
|     function (IdColumn) { | ||||
|         describe("IdColumn", function () { | ||||
|             var column; | ||||
|             var testIdMap, | ||||
|                 column; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 column = new IdColumn(); | ||||
|                 testIdMap = { "foo": "bar" }; | ||||
|                 column = new IdColumn(testIdMap); | ||||
|             }); | ||||
|  | ||||
|             it("has a name", function () { | ||||
| @@ -47,9 +49,9 @@ define( | ||||
|                     mockDomainObject.getId.andReturn(testId); | ||||
|                 }); | ||||
|  | ||||
|                 it("provides a domain object's identifier", function () { | ||||
|                 it("provides a value mapped from domain object's identifier", function () { | ||||
|                     expect(column.value(mockDomainObject)) | ||||
|                         .toEqual(testId); | ||||
|                         .toEqual(testIdMap[testId]); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|   | ||||
| @@ -23,13 +23,20 @@ | ||||
| define( | ||||
|     ['../../src/actions/ModeColumn'], | ||||
|     function (ModeColumn) { | ||||
|         var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f']; | ||||
|  | ||||
|         describe("ModeColumn", function () { | ||||
|             var testIndex, | ||||
|                 testIdMap, | ||||
|                 column; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testIndex = 3; | ||||
|                 column = new ModeColumn(testIndex); | ||||
|                 testIdMap = TEST_IDS.reduce(function (map, id, index) { | ||||
|                     map[id] = index; | ||||
|                     return map; | ||||
|                 }, {}); | ||||
|                 column = new ModeColumn(testIndex, testIdMap); | ||||
|             }); | ||||
|  | ||||
|             it("includes a one-based index in its name", function () { | ||||
| @@ -48,15 +55,15 @@ define( | ||||
|                     ); | ||||
|                     testModel = { | ||||
|                         relationships: { | ||||
|                             modes: ['a', 'b', 'c', 'd', 'e', 'f'] | ||||
|                             modes: TEST_IDS | ||||
|                         } | ||||
|                     }; | ||||
|                     mockDomainObject.getModel.andReturn(testModel); | ||||
|                 }); | ||||
|  | ||||
|                 it("returns a corresponding identifier", function () { | ||||
|                 it("returns a corresponding value from the map", function () { | ||||
|                     expect(column.value(mockDomainObject)) | ||||
|                         .toEqual(testModel.relationships.modes[testIndex]); | ||||
|                         .toEqual(testIdMap[testModel.relationships.modes[testIndex]]); | ||||
|                 }); | ||||
|  | ||||
|                 it("returns nothing when relationships are exceeded", function () { | ||||
|   | ||||
| @@ -75,7 +75,7 @@ define( | ||||
|                     return c === 'metadata' && testMetadata; | ||||
|                 }); | ||||
|  | ||||
|                 exporter = new TimelineColumnizer(mockDomainObjects); | ||||
|                 exporter = new TimelineColumnizer(mockDomainObjects, []); | ||||
|             }); | ||||
|  | ||||
|             describe("rows", function () { | ||||
| @@ -94,13 +94,6 @@ define( | ||||
|                 it("include one row per domain object", function () { | ||||
|                     expect(rows.length).toEqual(mockDomainObjects.length); | ||||
|                 }); | ||||
|  | ||||
|                 it("includes identifiers for each domain object", function () { | ||||
|                     rows.forEach(function (row, index) { | ||||
|                         var id = mockDomainObjects[index].getId(); | ||||
|                         expect(row.indexOf(id)).not.toEqual(-1); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("headers", function () { | ||||
|   | ||||
| @@ -214,23 +214,6 @@ define( | ||||
|  | ||||
|             }); | ||||
|  | ||||
|             it("reports full scrollable width using zoom controller", function () { | ||||
|                 var mockZoom = jasmine.createSpyObj('zoom', ['toPixels', 'duration']); | ||||
|                 mockZoom.toPixels.andReturn(54321); | ||||
|                 mockZoom.duration.andReturn(12345); | ||||
|  | ||||
|                 // Initially populate | ||||
|                 fireWatch('domainObject', mockDomainObject); | ||||
|  | ||||
|                 expect(controller.width(mockZoom)).toEqual(54321); | ||||
|                 // Verify interactions; we took zoom's duration for our start/end, | ||||
|                 // and converted it to pixels. | ||||
|                 // First, check that we used the start/end (from above) | ||||
|                 expect(mockZoom.duration).toHaveBeenCalledWith(12321 - 42); | ||||
|                 // Next, verify that the result was passed to toPixels | ||||
|                 expect(mockZoom.toPixels).toHaveBeenCalledWith(12345); | ||||
|             }); | ||||
|  | ||||
|             it("provides drag handles", function () { | ||||
|                 // TimelineDragPopulator et al are tested for these, | ||||
|                 // so just verify that handles are indeed exposed. | ||||
|   | ||||
| @@ -28,6 +28,7 @@ define( | ||||
|         describe("The timeline zoom state controller", function () { | ||||
|             var testConfiguration, | ||||
|                 mockScope, | ||||
|                 mockWindow, | ||||
|                 controller; | ||||
|  | ||||
|             beforeEach(function () { | ||||
| @@ -35,10 +36,16 @@ define( | ||||
|                     levels: [1000, 2000, 3500], | ||||
|                     width: 12321 | ||||
|                 }; | ||||
|                 mockScope = jasmine.createSpyObj("$scope", ['$watch']); | ||||
|                 mockScope = | ||||
|                     jasmine.createSpyObj("$scope", ['$watch', '$apply']); | ||||
|                 mockScope.commit = jasmine.createSpy('commit'); | ||||
|                 mockScope.scroll = { x: 0, width: 1000 }; | ||||
|                 mockWindow = { | ||||
|                     requestAnimationFrame: jasmine.createSpy('raf') | ||||
|                 }; | ||||
|                 controller = new TimelineZoomController( | ||||
|                     mockScope, | ||||
|                     mockWindow, | ||||
|                     testConfiguration | ||||
|                 ); | ||||
|             }); | ||||
| @@ -47,12 +54,6 @@ define( | ||||
|                 expect(controller.zoom()).toEqual(2000); | ||||
|             }); | ||||
|  | ||||
|             it("allows duration to be changed", function () { | ||||
|                 var initial = controller.duration(); | ||||
|                 controller.duration(initial * 3.33); | ||||
|                 expect(controller.duration() > initial).toBeTruthy(); | ||||
|             }); | ||||
|  | ||||
|             it("handles time-to-pixel conversions", function () { | ||||
|                 var zoomLevel = controller.zoom(); | ||||
|                 expect(controller.toPixels(zoomLevel)).toEqual(12321); | ||||
| @@ -70,11 +71,6 @@ define( | ||||
|                 expect(controller.zoom()).toEqual(3500); | ||||
|             }); | ||||
|  | ||||
|             it("observes scroll bounds", function () { | ||||
|                 expect(mockScope.$watch) | ||||
|                     .toHaveBeenCalledWith("scroll", jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             describe("when watches have fired", function () { | ||||
|                 var mockDomainObject, | ||||
|                     mockPromise, | ||||
| @@ -115,6 +111,10 @@ define( | ||||
|                     mockScope.$watch.calls.forEach(function (call) { | ||||
|                         call.args[1](mockScope[call.args[0]]); | ||||
|                     }); | ||||
|  | ||||
|                     mockWindow.requestAnimationFrame.calls.forEach(function (call) { | ||||
|                         call.args[0](); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("zooms to fit the timeline", function () { | ||||
| @@ -125,6 +125,27 @@ define( | ||||
|                     expect(Math.round(controller.toMillis(x2))) | ||||
|                         .toBeGreaterThan(testEnd); | ||||
|                 }); | ||||
|  | ||||
|                 it("provides a width which is not less than scroll area width", function () { | ||||
|                     var testPixel = mockScope.scroll.width / 4, | ||||
|                         testMillis = controller.toMillis(testPixel); | ||||
|                     expect(controller.width(testMillis)) | ||||
|                         .not.toBeLessThan(mockScope.scroll.width); | ||||
|                 }); | ||||
|  | ||||
|                 it("provides a width with some margin past timestamp", function () { | ||||
|                     var testPixel = mockScope.scroll.width * 4, | ||||
|                         testMillis = controller.toMillis(testPixel); | ||||
|                     expect(controller.width(testMillis)) | ||||
|                         .toBeGreaterThan(controller.toPixels(testMillis)); | ||||
|                 }); | ||||
|  | ||||
|                 it("provides a width which does not greatly exceed timestamp", function () { | ||||
|                     var testPixel = mockScope.scroll.width * 4, | ||||
|                         testMillis = controller.toMillis(testPixel); | ||||
|                     expect(controller.width(testMillis)) | ||||
|                         .toBeLessThan(controller.toPixels(testMillis * 2)); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|   | ||||
| @@ -61,8 +61,7 @@ define( | ||||
|                         { | ||||
|                             x: event.pageX - rect.left, | ||||
|                             y: event.pageY - rect.top | ||||
|                         }, | ||||
|                         domainObject | ||||
|                         } | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -34,8 +34,7 @@ define( | ||||
|             TEST_ID = "test-id", | ||||
|             DROP_ID = "drop-id"; | ||||
|  | ||||
|         //TODO: Disabled for NEM Beta | ||||
|         xdescribe("The drop gesture", function () { | ||||
|         describe("The drop gesture", function () { | ||||
|             var mockDndService, | ||||
|                 mockQ, | ||||
|                 mockElement, | ||||
| @@ -144,23 +143,6 @@ define( | ||||
|                 expect(mockCompose.perform).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|  | ||||
|             it("does not invoke compose on drop in browse mode for non-folders", function () { | ||||
|                 // Set the mockDomainObject to not have the editor capability | ||||
|                 mockDomainObject.hasCapability.andReturn(false); | ||||
|                 // Set the mockDomainObject to not have a type of folder | ||||
|                 mockDomainObject.getModel.andReturn({type: 'notAFolder'}); | ||||
|  | ||||
|                 callbacks.dragover(mockEvent); | ||||
|                 expect(mockAction.getActions).toHaveBeenCalledWith({ | ||||
|                     key: 'compose', | ||||
|                     selectedObject: mockDraggedObject | ||||
|                 }); | ||||
|                 callbacks.drop(mockEvent); | ||||
|                 expect(mockCompose.perform).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|  | ||||
|             it("invokes compose on drop in browse mode for folders", function () { | ||||
|                 // Set the mockDomainObject to not have the editor capability | ||||
|                 mockDomainObject.hasCapability.andReturn(false); | ||||
|   | ||||
| @@ -34,8 +34,8 @@ var EditItem = (function () { | ||||
|     EditItem.prototype.EditButton = function () { | ||||
|         return element.all(by.css('[ng-click="parameters.action.perform()"]')).filter(function (arg) { | ||||
|             return arg.getAttribute("title").then(function (title){ | ||||
|                 //expect(title).toEqual("Edit this object."); | ||||
|                 return title == 'Edit this object.'; | ||||
|                 //expect(title).toEqual("Edit"); | ||||
|                 return title == 'Edit'; | ||||
|             }) | ||||
|         }); | ||||
|     }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user