Compare commits

...

122 Commits

Author SHA1 Message Date
e8e9598721 [Build] Remove SNAPSHOT status
...to close sprint Kress,
https://github.com/nasa/openmct/milestones/Kress
2016-06-24 15:37:23 -07:00
ce87ad2564 Merge pull request #916 from ev1stensberg/patch-2
[Branding] Added Apache license to README.md
2016-06-23 11:43:16 -07:00
2a2f6e8142 Merge pull request #915 from ev1stensberg/patch-1
[Functionality] Log app instance to the console
2016-06-23 11:24:53 -07:00
3e5057c285 Merge pull request #1015 from mockingjamie/master
Proofread readme.md
2016-06-23 11:24:04 -07:00
5485950130 Merge pull request #1049 from nasa/update-plot-squish-margin
[Plot] Set min height
2016-06-22 11:36:19 -07:00
c8f4568bd0 [Plot] Set min height
Set the min height for the plot element based on user feedback for
minimum plot size that they find useful.  Plot ticks may overlap
but that is expected to be fixed in a future release.

Fixes https://github.com/nasa/openmct/issues/1048
2016-06-22 10:51:16 -07:00
cefb40856b Merge pull request #1042 from nasa/open1040
[Tables] Enabled auto-scroll by default fixes #1040
2016-06-20 16:24:58 -07:00
ee7c450e11 [Tables] Enabled auto-scroll by default fixes #1040 2016-06-20 16:18:26 -07:00
d741e0f23c Merge pull request #1041 from nasa/frontend-1014a
Emphasis styling applied to Edit button properly
2016-06-20 16:13:29 -07:00
8080490e5c [Frontend] Applied emphasis colors to .key-properties
fixes #1014
Added .key-properties to .major to color the Edit
button as intended;
2016-06-20 16:06:23 -07:00
a3443d8077 Merge pull request #1025 from nasa/frontend-1019
R&I fixes for Snow button colors and Timeline edit Add menu
2016-06-20 12:36:29 -07:00
53e8e7c688 Merge pull request #1024 from nasa/plots-1022
R&I un-squished plots fix
2016-06-17 10:36:40 -07:00
dea94e4e68 [Frontend] Fix button colors in Snow theme
Fixes #1014
- Problem was actually application of a color
within the .icon class - fix removes that
definition;
2016-06-16 17:55:40 -07:00
d40c7f1821 [Frontend] Restore the Add menu to edit Timelines
Fixes #1019
- Removed overflow: hidden from .l-edit-controls;
2016-06-16 17:46:15 -07:00
c7e7e0c302 [Frontend] Fixes to un-squish plots
Fixes #1022
- min-height added to .gl-plot element;
- Moved plot value definitions out of _plots-main
and into _constants and normalized naming;
2016-06-16 17:24:44 -07:00
8ac6ac97f0 Merge remote-tracking branch 'origin/open972' 2016-06-16 11:08:43 -07:00
1afa4ab329 Merge pull request #1017 from nasa/open664
[Edit Mode] Canceling edit mode with unsaved changes now shows confirmation dialog to user
2016-06-16 09:31:37 -07:00
c2517c1670 [Edit Mode] Canceling edit mode with unsaved changes now shows confirmation dialog to user. Fixes #664 2016-06-15 17:10:29 -07:00
717ceff02c Merge pull request #995 from nasa/timeline-913a
R&I tweaks to Timeline scrollbar and splitters
2016-06-15 11:45:17 -07:00
6878c59f45 Merge pull request #1016 from nasa/fix-1013
[Persistence] Use identifier service to get key
2016-06-15 11:18:13 -07:00
c00b053aa7 [Tests] verify that identifier service provides key
Verify that the results of the identifier service are used for
persistence calls instead of the domain object id.

https://github.com/nasa/openmct/issues/1013
2016-06-15 10:31:25 -07:00
480e12c3ab Merge pull request #986 from nasa/table-export-934
[Table] Add Export for table contents
2016-06-15 10:17:21 -07:00
50bd233b0a Merge remote-tracking branch 'origin/master' into table-export-934
Conflicts:
	platform/commonUI/general/res/sass/controls/_buttons.scss
	platform/features/table/res/templates/mct-table.html
2016-06-15 10:05:20 -07:00
79406cf1ed [Tables] #972 Refactored to simplify code. Fixes #972 2016-06-14 16:34:07 -07:00
2d5824c4ab [Persistence] Use identifier service to get key
Retrieve key from identifier service for all persist operations.

Fixes https://github.com/nasa/openmct/issues/1013
2016-06-14 10:56:54 -07:00
3480809129 Merge remote-tracking branch 'origin/open629' 2016-06-14 10:44:58 -07:00
11bc48c7e0 Merge remote-tracking branch 'origin/timeline-console-error-978' 2016-06-14 10:41:26 -07:00
d759401b69 Merge remote-tracking branch 'origin/open1000' 2016-06-14 10:38:51 -07:00
5a2e5ac48f Merge pull request #1 from mockingjamie/changes-to-readme.md
Proofread readme.md
2016-06-13 21:27:43 -04:00
1144f818cf Proofread readme.md
Made minor grammatical corrections.
2016-06-13 21:27:28 -04:00
0aebecfbb0 Merge pull request #987 from nasa/anim-to-edit-709
[Frontend] Animation added to transition from browse to edit mode
2016-06-13 13:31:49 -07:00
531d40993a Merge remote-tracking branch 'origin/better-dialog-message-339' 2016-06-10 14:55:07 -07:00
4c097faf88 Merge remote-tracking branch 'origin/timeline-flash-997' 2016-06-10 14:50:36 -07:00
5152e64895 [Duplicate] Allow copy across spaces
Fixes #1007
2016-06-10 11:38:55 -07:00
0263237b2c Merge pull request #1006 from nasa/open861
[Tables] Recalculate column dimensions on resize
2016-06-09 10:21:05 -07:00
acd0fae040 [Tables] Recalculate column dimensions on resize. Fixes #861 2016-06-09 12:59:46 +01:00
29dd51439d [Date Input] Addressed issues with date selector. Fixes #1000 2016-06-08 17:37:26 +01:00
a1b2175801 [Timeline] Reduce flicker
Reposition scroll bar in Timeline with RAF instead of timeout;
this ensures that scroll bar is positioned after the current
digest (updating the width) but before the results are rendered
(avoiding flicker.) Fixes #997
2016-06-07 10:05:51 -07:00
b0f06a2195 [Build] Restore SNAPSHOT status
...to open sprint Kress,
https://github.com/nasa/openmct/milestones/Kress
2016-06-06 09:57:09 -07:00
f9c93ca022 [Build] Remove SNAPSHOT status
To close sprint Huxley,
https://github.com/nasa/openmct/milestones/Huxley
2016-06-06 09:49:52 -07:00
8e0858bb24 [Frontend] Tweaks to splitter dimensions
Fixes #913
- Tightened up splitter height and width;
- Added hover color for snow theme;
2016-06-03 10:00:16 -07:00
ee0fa0451a Merge branch 'master' into timeline-zoom-center-936
Conflicts:
	platform/features/timeline/src/controllers/TimelineZoomController.js
	platform/features/timeline/test/controllers/TimelineZoomControllerSpec.js
2016-06-03 09:18:01 -07:00
e86e955682 Merge pull request #993 from nasa/revert-990-timeline-regression-817
Revert "[Timeline] Provide greater initial width"
2016-06-02 17:05:11 -07:00
9913fb48f5 Revert "[Timeline] Provide greater initial width" 2016-06-02 16:54:59 -07:00
71c362f016 Merge pull request #992 from nasa/revert-991-timeline-regression-817
Revert "[Timeline] Update scroll position on timeout"
2016-06-02 16:54:48 -07:00
fa6e8fd5f9 Revert "[Timeline] Update scroll position on timeout" 2016-06-02 16:49:49 -07:00
23b64951f3 [Timeline] Update zoom controller spec
...to reflect changes/simplifications for #936.
2016-06-02 16:42:09 -07:00
99590d18f7 [Timeline] Simplify bounds-tracking 2016-06-02 16:40:07 -07:00
86b31bc040 [Timeline] Simplify scroll-setting 2016-06-02 16:38:11 -07:00
d52bfed1df [Timeline] Always set scroll on timeout
...to allow time for width to increase.
2016-06-02 16:36:09 -07:00
808ccd0376 [Frontend] Tweaks to splitter
Fixes #913
- IN PROGRESS: working on making
smaller splitter in Timelines
2016-06-02 16:23:42 -07:00
44d6456de1 [Timeline] Set scroll on timeout
Whenever timeline zoom controller sets scroll, check to see if
there may be a width violation that causes scroll to be reset,
and retry on a timeout if so. Fixes #936.
2016-06-02 16:05:28 -07:00
a394b95259 [Frontend] Tweaks to scrollbar visibility
Fixes #913
- Scrollbar now always visible in the right side
of the tabular area, in order to avoid column
bottom edges not aligning;
2016-06-02 16:04:26 -07:00
beeefe517a Merge pull request #991 from nasa/timeline-regression-817
[Timeline] Update scroll position on timeout
2016-06-02 15:37:42 -07:00
d02f4041b2 [Timeline] Update scroll position on timeout
Fixes #817
2016-06-02 15:34:32 -07:00
9fd75ff91e Merge pull request #990 from nasa/timeline-regression-817
[Timeline] Provide greater initial width
2016-06-02 14:59:14 -07:00
026ece3956 [Timeline] Provide greater initial width
This avoids starting with a scrollable width too small for the
initial scroll position that the zoom controller selects.
Fixes #817
2016-06-02 14:48:58 -07:00
214a843dba [Frontend] New message for copy in-progress dialog
fixes #339
2016-06-02 11:09:22 -07:00
1d6880c283 Merge pull request #982 from nasa/timeline-scroll-981
[Timeline] Use reasonable width for scroll area
2016-06-02 16:59:08 +01:00
8ddad9bf4c Merge pull request #984 from nasa/open979
[Edit] Fixed issue with cancel action throwing an error. Fixes #979
2016-06-02 10:31:20 +01:00
f167022eea Changed logic of persisted check slightly 2016-06-02 10:26:38 +01:00
76edba1014 [Frontend] Mods to Edit button
fixes #709
- Changed title and style of main Edit button;
- Updated EditItem.js protractor ID accordingly;
2016-06-01 19:19:58 -07:00
7904989a23 [Frontend] Added transitional animation to Edit mode
fixes #709
- When going from browse to edit mode, the wrapper
around the object being edited will now transition in
from its edges, and the edit controls toolbar will
animate its height;
- There is no transition applied for going from edit
to browse; to do this we'd need to mod the JS on
exiting to look for the end of an animation event;
- Tested in Chrome, Safari and Firefox;
- May not be smooth with very complex objects
like Layouts with a large number of components;
- Added transitional animations to .l-object-wrapper
and .l-edit-controls;
- New 'animTo' mixin added to _effects.scss;
2016-06-01 19:18:55 -07:00
ea676b4368 Merge remote-tracking branch 'origin/master' into table-export-934 2016-06-01 10:33:53 -07:00
cc2b102256 Merge pull request #985 from nasa/open973
[Style] Export button in tabular views
2016-06-01 10:27:51 -07:00
b1266abf01 [Edit] Fixed issue with cancel action throwing an error. Fixes #979 2016-06-01 10:41:01 +01:00
cc7d0477e8 [Timeline] Check for existence of timespan
...before attempting to calculate a width based on it.
Fixes #978.
2016-05-31 16:41:30 -07:00
5a2d1a746d [Timeline] Add missing semicolon 2016-05-31 16:27:59 -07:00
4f0e3fdf85 [Timeline] Test zoom controller's width 2016-05-31 16:21:40 -07:00
be9f56107c [Timeline] Remove obsolete test cases 2016-05-31 16:15:57 -07:00
787f3815df [Timeline] Expose width from ZoomController
...and ensure that the width exposed is not excessively
large; fixes #981
2016-05-31 16:12:49 -07:00
35d7d9b380 [Timeline] Remove width method
...from TimelineController. Will replace with a more straightforward
call to the zoom controller that uses the exposed end time instead,
to address #981.
2016-05-31 16:05:06 -07:00
8b9c51f303 [Frontend] Styling Export button
fixes #973
- Done;
- Styling for Export button and tabular view
area in layout frame context;
- Export button in frame context now
hidden until user hovers over tabular
view area in frame, includes animated
transition;
- Normalized line-height on button and
menu elements in frame context;
- Layout/markup/SASS for historical and
RT tabular view modified;
- Converted imagery.html layout
to use flexbox;
2016-05-31 11:40:31 -07:00
661b3d5889 [Frontend] Styling Export button
fixes #973
- In progress:
- Added new download symbol to symbols font;
- Added export symbol to export button;
- Layout/markup/SASS for historical tabular
view modified;
2016-05-31 09:50:12 -07:00
01c85cb58d [Table] #972 Further refactoring, tests, style fixes 2016-05-31 12:05:40 +01:00
0218f42e2b removed redundant code 2016-05-27 15:25:06 -07:00
d8d0f22889 Second rewrite 2016-05-27 15:14:46 -07:00
d8a097a95a [Tables] Added timeouts to yield control to ui thread. Fixes #972 2016-05-27 15:14:45 -07:00
dc577d4c24 Merge pull request #974 from nasa/open970
R&I open970: hide nav-to-parent arrow when in Edit mode
2016-05-27 14:56:48 -07:00
8f9308de01 Merge pull request #962 from nasa/csv-export-update-751
[Timeline] Updates to CSV Export
2016-05-27 14:55:59 -07:00
8f7a5e113b Merge pull request #951 from nasa/orphan-navigation-765
[Navigation] Prevent navigation to orphan objects
2016-05-27 14:53:33 -07:00
9820f9d9c5 [Frontend] Mod CSS to properly hide nav-to-parent when editing
fixes #970
Not sure what problem was, but betting this was due to removal
of an ng-class previously in markup;
2016-05-26 12:45:25 -07:00
56ff98cce7 Merge branch 'master' into orphan-navigation-765 2016-05-26 12:35:48 -07:00
dade6b2254 [Timeline] Use positive logic for clarity
https://github.com/nasa/openmct/pull/962#discussion_r64678013
2016-05-26 12:12:52 -07:00
e9cac6eff3 [Timeline] Add JSDoc for new parameter
https://github.com/nasa/openmct/pull/962#discussion_r64677520
2016-05-26 12:12:52 -07:00
7fc2fcfa07 [Edit Mode] #629 Rewrote obsolete DomainColumn tests 2016-05-26 11:55:13 -07:00
5689279954 [Timeline] Add JSDoc for idMap
https://github.com/nasa/openmct/pull/962#discussion_r64676750
https://github.com/nasa/openmct/pull/962#discussion_r64677198
2016-05-26 11:53:16 -07:00
165e158f37 [Edit Mode] #629 Removed redundant test from DomainObjectProviderSpec 2016-05-26 11:44:43 -07:00
f301741852 [Edit Mode] #629 restored disabled tests for DropGesture. Removed extraneous argument to broadcast function 2016-05-26 11:42:15 -07:00
eda1f23df9 [Edit Mode] #629 rewriting disabled tests 2016-05-26 11:38:12 -07:00
d15d27af73 Merge branch 'master' into table-export-934 2016-05-26 10:42:35 -07:00
438511c5f7 [Table] Test CSV Export behavior 2016-05-25 15:07:17 -07:00
3eb960cf5a [Table] Move export button out of scroll 2016-05-25 14:50:06 -07:00
699f6ba458 [Table] Export table contents as CSV 2016-05-25 13:54:10 -07:00
f21f22d95c [Table] Tweak appearance of export button 2016-05-25 13:43:44 -07:00
b520d08818 [Table] Begin work on CSV export
#934
2016-05-25 13:23:19 -07:00
f9fd97230f [Timeline] Satisfy JSHint 2016-05-25 12:37:03 -07:00
536e2290b8 Merge branch 'master' into csv-export-update-751 2016-05-25 12:33:43 -07:00
73b922facf [Timeline] Update specs for indexes instead of ids 2016-05-25 12:32:44 -07:00
ba0d9a186b [Timeline] Account for new argument in spec 2016-05-25 12:26:47 -07:00
80f5cb756d [Timeline] Account for new argument in spec 2016-05-25 12:25:58 -07:00
d7f566088f [Timeline] Update spec to include logging 2016-05-25 12:25:02 -07:00
a3bcaea7f9 [Timeline] Show units in utilization headers 2016-05-25 12:21:58 -07:00
23c71b7218 [Timeline] Include units for utilizations 2016-05-25 12:12:11 -07:00
463f7ccf65 [Timeline] Use indexes instead of UUIDs 2016-05-25 12:07:35 -07:00
87fe407739 Merge branch 'master' into csv-export-update-751 2016-05-25 12:00:43 -07:00
bb4f1ce7cd [Timeline] Include utilization columns 2016-05-25 10:52:25 -07:00
0cc2ba7595 [Timeline] Import UtilizationColumn 2016-05-25 10:38:00 -07:00
8162429106 [Timeline] Pass in resources extensions 2016-05-25 10:37:01 -07:00
ed519d89d7 [Timeline] Log errors during CSV export
#751
2016-05-25 10:35:29 -07:00
0e4f6185b8 Merge branch 'master' into csv-export-update-751
Conflicts:
	platform/features/timeline/bundle.js
2016-05-25 10:29:56 -07:00
1ced47fc2c [Navigation] Prevent navigation to orphan objects
This is particularly useful when a persistence failure has caused
a created object not to be added to its parent container. #765
2016-05-23 14:07:09 -07:00
677b65d124 Optional indent removed
removed whitespace
2016-05-11 00:39:16 +02:00
31d31d7819 Removed indents
Removed unnecessary indentations
2016-05-11 00:14:52 +02:00
d32ef4bc3d Last indentation fix 2016-05-10 22:03:07 +02:00
b4faf8991d Indentation Fixes 2016-05-10 22:02:19 +02:00
0564759481 Swapped order of license
Swaped license order with name
2016-05-10 21:48:24 +02:00
24e391edf7 [Branding] Added Apache license to README.md
Added Apache license
2016-05-10 21:44:10 +02:00
cf295105d4 [Functionality] Log app instance to the console
Added logging statement, so the user knows when the app is up and running.
2016-05-10 21:06:04 +02:00
f16a107105 [Timeline] Inject resources into CSV action 2016-04-15 08:51:22 -07:00
f683ca44a2 [Timeline] Read resource utilizations during CSV export 2016-04-15 08:45:42 -07:00
546cde56a8 [Timeline] Expose internal resource utilization
...to allow this to be exported for CSV, #751
2016-04-15 08:32:34 -07:00
82 changed files with 3964 additions and 2377 deletions

View File

@ -1,4 +1,4 @@
# Open MCT
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](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
View File

@ -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)
});
}());

View File

@ -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",

View File

@ -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": [

View File

@ -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...

View File

@ -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)
);
};

View File

@ -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;
});

View File

@ -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};

View File

@ -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 () {

View File

@ -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();
});
});
});
});
});
});
});

View File

@ -170,7 +170,7 @@ define([
"navigationService",
"$log"
],
"description": "Edit this object.",
"description": "Edit",
"category": "view-control",
"glyph": "p"
},

View File

@ -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);
};
/**

View File

@ -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()) {

View File

@ -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");
});
});
}

View File

@ -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();

View File

@ -100,5 +100,6 @@
<glyph unicode="&#xe620;" 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="&#xe621;" 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="&#xe622;" 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="&#xe623;" 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="&#xe642;" 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

View File

@ -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';

View File

@ -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);
}

View File

@ -348,7 +348,6 @@
display: inline-block;
font-family: 'symbolsfont';
margin-left: $interiorMarginSm;
vertical-align: top;
}
@mixin nice-textarea($bg: $colorBodyBg, $fg: $colorBodyFg) {

View File

@ -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;
}
}

View File

@ -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";
}

View File

@ -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;
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -54,7 +54,8 @@
height: $ohH;
line-height: $ohH;
padding: 0 $interiorMargin;
> span {
> span,
&:before {
font-size: 0.65rem;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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'"

View File

@ -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;

View File

@ -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);
});
}
}

View File

@ -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);
});

View File

@ -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

View File

@ -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

View File

@ -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;
}
);

View File

@ -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
);
});

View File

@ -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);
});
});
}
);

View File

@ -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"
});

View File

@ -24,10 +24,7 @@ define(
[],
function () {
var DISALLOWED_ACTIONS = [
"move",
"copy"
];
var DISALLOWED_ACTIONS = ["move"];
/**
* This policy prevents performing move/copy/link actions across

View File

@ -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);
});
});

View File

@ -42,7 +42,7 @@ define(
'parent'
];
xdescribe("ConductorRepresenter", function () {
describe("ConductorRepresenter", function () {
var mockThrottle,
mockConductorService,
mockCompile,

View File

@ -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">&lt;</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>

View File

@ -109,7 +109,7 @@ define([
{
"key": "HistoricalTableController",
"implementation": HistoricalTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter"]
"depends": ["$scope", "telemetryHandler", "telemetryFormatter", "$timeout"]
},
{
"key": "RealtimeTableController",

View File

@ -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>

View File

@ -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>

View File

@ -4,6 +4,7 @@
rows="rows"
enableFilter="true"
enableSort="true"
auto-scroll="autoScroll">
class="tabular-holder t-exportable"
auto-scroll="true">
</mct-table>
</div>

View File

@ -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;

View File

@ -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);
}
/**

View File

@ -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;

View File

@ -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();
}

View File

@ -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,

View File

@ -81,7 +81,13 @@ define(
return {
restrict: "E",
template: TableTemplate,
controller: ['$scope', '$timeout', '$element', MCTTableController],
controller: [
'$scope',
'$timeout',
'$element',
'exportService',
MCTTableController
],
scope: {
headers: "=",
rows: "=",

View File

@ -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();
});
});

View File

@ -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);
});
});
});
}
);

View File

@ -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;

View File

@ -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);
});
});
}
);

View File

@ -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"
]
},

View File

@ -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;
}
}
}

View File

@ -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 ? {

View File

@ -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>

View File

@ -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;

View File

@ -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");
});

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;
});

View File

@ -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

View File

@ -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

View File

@ -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);
}
};
}

View File

@ -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 () {

View File

@ -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);
});
});
});
});

View File

@ -52,6 +52,7 @@ define(
task = new ExportTimelineAsCSVTask(
mockExportService,
[],
mockDomainObject
);
});

View File

@ -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]);
});
});

View File

@ -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 () {

View File

@ -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 () {

View File

@ -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.

View File

@ -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));
});
});
});

View File

@ -61,8 +61,7 @@ define(
{
x: event.pageX - rect.left,
y: event.pageY - rect.top
},
domainObject
}
);
}
}

View File

@ -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);

View File

@ -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';
})
});
};