Compare commits

..

1 Commits

Author SHA1 Message Date
Henry
0028401778 [Edit Mode] Added some missing mock behavior to address failing test. Fixes #1361 2016-12-20 14:25:03 -08:00
211 changed files with 4004 additions and 4508 deletions

64
API.md
View File

@@ -129,10 +129,8 @@ provider.
The "composition" of a domain object is the list of objects it contains,
as shown (for example) in the tree for browsing. Open MCT provides a
[default solution](#default-composition-provider) for composition, but there
may be cases where you want to provide the composition of a certain object
(or type of object) dynamically.
default solution for composition, but there may be cases where you want
to provide the composition of a certain object (or type of object) dynamically.
For instance, you may want to populate a hierarchy under a custom root-level
object based on the contents of a telemetry dictionary.
To do this, you can add a new CompositionProvider:
@@ -148,29 +146,6 @@ openmct.composition.addProvider({
});
```
#### Default Composition Provider
The default composition provider applies to any domain object with
a `composition` property. The value of `composition` should be an
array of identifiers, e.g.:
```js
var domainObject = {
name: "My Object",
type: 'folder',
composition: [
{
key: '412229c3-922c-444b-8624-736d85516247',
namespace: 'foo'
},
{
key: 'd6e0ce02-5b85-4e55-8006-a8a505b64c75',
namespace: 'foo'
}
]
};
```
### Adding Telemetry Providers
When connecting to a new telemetry source, you will want to register a new
@@ -290,41 +265,6 @@ openmct.install(myPlugin);
The plugin will be invoked to configure Open MCT before it is started.
### Included Plugins
Open MCT is packaged along with a few general-purpose plugins:
* `openmct.plugins.CouchDB` is an adapter for using CouchDB for persistence
of user-created objects. This is a constructor that takes the URL for the
CouchDB database as a parameter, e.g.
`openmct.install(new openmct.plugins.CouchDB('http://localhost:5984/openmct'))`
* `openmct.plugins.Elasticsearch` is an adapter for using Elasticsearch for
persistence of user-created objects. This is a
constructor that takes the URL for the Elasticsearch instance as a
parameter, e.g.
`openmct.install(new openmct.plugins.CouchDB('http://localhost:9200'))`.
Domain objects will be indexed at `/mct/domain_object`.
* `openmct.plugins.espresso` and `openmct.plugins.snow` are two different
themes (dark and light) available for Open MCT. Note that at least one
of these themes must be installed for Open MCT to appear correctly.
* `openmct.plugins.localStorage` provides persistence of user-created
objects in browser-local storage. This is particularly useful in
development environments.
* `openmct.plugins.myItems` adds a top-level folder named "My Items"
when the application is first started, providing a place for a
user to store created items.
* `openmct.plugins.utcTimeSystem` provides support for using the time
conductor with UTC time.
Generally, you will want to either install these plugins, or install
different plugins that provide persistence and an initial folder
hierarchy. Installation is as described [above](#installing-plugins):
```
openmct.install(openmct.plugins.localStorage);
openmct.install(openmct.plugins.myItems);
```
### Writing Plugins
Plugins configure Open MCT, and should utilize the

View File

@@ -1,6 +1,6 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
Open MCT is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)

View File

@@ -131,7 +131,7 @@ Keeping that in mind, there are a few useful patterns supported by the
framework that are useful to keep in mind.
The specific service infrastructure provided by the platform is described
in the [Platform Architecture](platform.md).
in the [Platform Architecture](Platform.md).
## Extension Categories

View File

@@ -1338,6 +1338,55 @@ are supported:
Open MCT defines several Angular directives that are intended for use both
internally within the platform, and by plugins.
## Before Unload
The `mct-before-unload` directive is used to listen for (and prompt for user
confirmation) of navigation changes in the browser. This includes reloading,
following links out of Open MCT, or changing routes. It is used to hook into
both `onbeforeunload` event handling as well as route changes from within
Angular.
This directive is useable as an attribute. Its value should be an Angular
expression. When an action that would trigger an unload and/or route change
occurs, this Angular expression is evaluated. Its result should be a message to
display to the user to confirm their navigation change; if this expression
evaluates to a falsy value, no message will be displayed.
## Chart
The `mct-chart` directive is used to support drawing of simple charts. It is
present to support the Plot view, and its functionality is limited to the
functionality that is relevant for that view.
This directive is used at the element level and takes one attribute, `draw`
which is an Angular expression which will should evaluate to a drawing object.
This drawing object should contain the following properties:
* `dimensions`: The size, in logical coordinates, of the chart area. A
two-element array or numbers.
* `origin`: The position, in logical coordinates, of the lower-left corner of
the chart area. A two-element array or numbers.
* `lines`: An array of lines (e.g. as a plot line) to draw, where each line is
expressed as an object containing:
* `buffer`: A Float32Array containing points in the line, in logical
coordinates, in sequential x,y pairs.
* `color`: The color of the line, as a four-element RGBA array, where
each element is a number in the range of 0.0-1.0.
* `points`: The number of points in the line.
* `boxes`: An array of rectangles to draw in the chart area. Each is an object
containing:
* `start`: The first corner of the rectangle, as a two-element array of
numbers, in logical coordinates.
* `end`: The opposite corner of the rectangle, as a two-element array of
numbers, in logical coordinates. color : The color of the line, as a
four-element RGBA array, where each element is a number in the range of
0.0-1.0.
While `mct-chart` is intended to support plots specifically, it does perform
some useful management of canvas objects (e.g. choosing between WebGL and Canvas
2D APIs for drawing based on browser support) so its usage is recommended when
its supported drawing primitives are sufficient for other charting tasks.
## Container
@@ -2261,7 +2310,10 @@ The platform understands the following policy categories (specifiable as the
* `action`: Determines whether or not a given action is allowable. The candidate
argument here is an Action; the context is its action context object.
* `composition`: Determines whether or not domain objects of a given type (first argument, `parentType`) can contain a given object (second argument, `child`).
* `composition`: Determines whether or not domain objects of a given type are
allowed to contain domain objects of another type. The candidate argument here
is the container's `Type`; the context argument is the `Type` of the object to be
contained.
* `view`: Determines whether or not a view is applicable for a domain object.
The candidate argument is the view's extension definition; the context argument
is the `DomainObject` to be viewed.

View File

@@ -129,7 +129,7 @@ We will create this file in the directory tutorials/todo (we can hereafter refer
to this plugin as tutorials/todo as well.) We will start with an "empty bundle",
one which exposes no extensions - which looks like:
```js
```diff
define([
'openmct'
], function (
@@ -154,7 +154,7 @@ The tutorials will be updated with the new bundle registration mechanism once it
has been finalized.
#### Before
```html
```diff
<!--
Open MCT, Copyright (c) 2014-2016, United States Government
as represented by the Administrator of the National Aeronautics and Space
@@ -219,7 +219,7 @@ __index.html__
#### After
```html
```diff
<!--
Open MCT, Copyright (c) 2014-2016, United States Government
as represented by the Administrator of the National Aeronautics and Space
@@ -305,7 +305,7 @@ In the case of our to-do list feature, the to-do list itself is the thing we'll
want users to be able to create and edit. So, we will add that as a new type in
our bundle definition:
```js
```diff
define([
'openmct'
], function (
@@ -320,7 +320,7 @@ define([
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "cssClass": "icon-check",
+ "cssclass": "icon-check",
+ "description": "A list of things that need to be done.",
+ "features": ["creation"]
+ }
@@ -340,9 +340,8 @@ Going through the properties we've defined:
domain objects of this type.
* The `name` of "To-Do List" is the human-readable name for this type, and will
be shown to users.
* The `cssClass` maps to an icon that will be shown for each To-Do List. The icons
are defined in our [custom open MCT icon set](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss).
A complete list of available icons will be provided in the future.
* The `glyph` refers to a special character in Open MCT's custom font set;
this will be used as an icon.
* The `description` is also human-readable, and will be used whenever a longer
explanation of what this type is should be shown.
* Finally, the `features` property describes some special features of objects of
@@ -370,7 +369,7 @@ directory `tutorials/todo/res/templates` (`res` is, by default, the directory
where bundle-related resources are kept, and `templates` is where HTML templates
are stored by convention.)
```html
```diff
<div>
<a href="">All</a>
<a href="">Incomplete</a>
@@ -402,7 +401,7 @@ boolean `completed` flag.
To expose this view in Open MCT, we need to declare it in our bundle
definition:
```js
```diff
define([
'openmct'
], function (
@@ -416,7 +415,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssClass": "icon-check",
"cssclass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"]
}
@@ -425,7 +424,7 @@ define([
+ {
+ "key": "example.todo",
+ "type": "example.todo",
+ "cssClass": "icon-check",
+ "cssclass": "icon-check",
+ "name": "List",
+ "templateUrl": "templates/todo.html",
+ "editable": true
@@ -447,7 +446,7 @@ the domain object type, but could have chosen any unique name.
domain objects of that type. This means that we'll see this view for To-do Lists
that we create, but not for other domain objects (such as Folders.)
* The `cssClass` and `name` properties describe the icon and human-readable name
* The `glyph` and `name` properties describe the icon and human-readable name
for this view to display in the UI where needed (if multiple views are available
for To-do Lists, the user will be able to choose one.)
@@ -459,7 +458,7 @@ the user to create these yet. As a temporary workaround to test the view, we
will specify an initial state for To-do List domain object models in the
definition of that type.
```js
```diff
define([
'openmct'
], function (
@@ -473,7 +472,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssClass": "icon-check",
"cssclass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
+ "model": {
@@ -488,7 +487,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssClass": "icon-check",
"cssclass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true
@@ -530,7 +529,7 @@ in the directory `tutorials/todo/src/controllers` (`src` is, by default, the
directory where bundle-related source code is kept, and controllers is where
Angular controllers are stored by convention.)
```js
```diff
define(function () {
function TodoController($scope) {
var showAll = true,
@@ -595,7 +594,7 @@ prior to our template being utilized.
On its own, this controller merely exposes these functions; the next step is to
use them from our template:
```html
```diff
+ <div ng-controller="TodoController">
<div>
+ <a ng-click="setVisibility(true)">All</a>
@@ -631,7 +630,7 @@ If we were to try to run at this point, we'd run into problems because the
`TodoController` has not been registered with Angular. We need to first declare
it in our bundle definition, as an extension of category `controllers`:
```js
```diff
define([
'openmct',
+ './src/controllers/TodoController'
@@ -647,7 +646,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssClass": "icon-check",
"cssclass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
"model": {
@@ -662,7 +661,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssClass": "icon-check",
"cssclass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true
@@ -725,7 +724,7 @@ An Editing user interface is typically handled in a tool bar associated with a
view. The contents of this tool bar are defined declaratively in a view's
extension definition.
```js
```diff
define([
'openmct',
'./src/controllers/TodoController'
@@ -741,7 +740,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssClass": "icon-check",
"cssclass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
"model": {
@@ -756,7 +755,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssClass": "icon-check",
"cssclass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true,
@@ -766,7 +765,7 @@ define([
+ "items": [
+ {
+ "text": "Add Task",
+ "cssClass": "icon-plus",
+ "cssclass": "icon-plus",
+ "method": "addTask",
+ "control": "button"
+ }
@@ -775,7 +774,7 @@ define([
+ {
+ "items": [
+ {
+ "cssClass": "icon-trash",
+ "cssclass": "icon-trash",
+ "method": "removeTask",
+ "control": "button"
+ }
@@ -814,7 +813,7 @@ all the applicable controls, which means no controls at all.
To support selection, we will need to make some changes to our controller:
```js
```diff
define(function () {
+ // Form to display when adding new tasks
+ var NEW_TASK_FORM = {
@@ -929,7 +928,7 @@ Additionally, we need to make changes to our template to select specific tasks
in response to some user gesture. Here, we will select tasks when a user clicks
the description.
```html
```diff
<div ng-controller="TodoController">
<div>
<a ng-click="setVisibility(true)">All</a>
@@ -955,7 +954,7 @@ __tutorials/todo/res/templates/todo.html__
Finally, the `TodoController` uses the `dialogService` now, so we need to
declare that dependency in its extension definition:
```js
```diff
define([
'openmct',
'./src/controllers/TodoController'
@@ -971,7 +970,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssClass": "icon-check",
"cssclass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
"model": {
@@ -986,7 +985,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssClass": "icon-check",
"cssclass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true,
@@ -996,7 +995,7 @@ define([
"items": [
{
"text": "Add Task",
"cssClass": "icon-plus",
"cssclass": "icon-plus",
"method": "addTask",
"control": "button"
}
@@ -1005,7 +1004,7 @@ define([
{
"items": [
{
"cssClass": "icon-trash",
"cssclass": "icon-trash",
"method": "removeTask",
"control": "button"
}
@@ -1059,7 +1058,7 @@ In this section, our goal is to:
To support the first two, we'll need to expose some methods for checking these
states in the controller:
```js
```diff
define(function () {
// Form to display when adding new tasks
var NEW_TASK_FORM = {
@@ -1176,7 +1175,7 @@ states visually, and to generally improve the appearance of our view. We add
another file to the res directory of our bundle; this time, it is `css/todo.css`
(with the `css` directory again being a convention.)
```css
```diff
.example-todo div.example-button-group {
margin-top: 12px;
margin-bottom: 12px;
@@ -1220,7 +1219,7 @@ To include this CSS file in our running instance of Open MCT, we need to
declare it in our bundle definition, this time as an extension of category
`stylesheets`:
```js
```diff
define([
'openmct',
'./src/controllers/TodoController'
@@ -1236,7 +1235,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssClass": "icon-check",
"cssclass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
"model": {
@@ -1248,7 +1247,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssClass": "icon-check",
"cssclass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true,
@@ -1258,7 +1257,7 @@ define([
"items": [
{
"text": "Add Task",
"cssClass": "icon-plus",
"cssclass": "icon-plus",
"method": "addTask",
"control": "button"
}
@@ -1267,7 +1266,7 @@ define([
{
"items": [
{
"cssClass": "icon-trash",
"cssclass": "icon-trash",
"method": "removeTask",
"control": "button"
}
@@ -1300,7 +1299,7 @@ To-Do List's type above; now To-Do Lists will start off empty.
Finally, let's utilize these changes from our view's template:
```html
```diff
+ <div ng-controller="TodoController" class="example-todo">
+ <div class="example-button-group">
+ <a ng-class="{ selected: checkVisibility(true) }"
@@ -1360,7 +1359,7 @@ We'll also be defining some custom styles, so we'll include that extension as
well. We'll be creating this plugin in `tutorials/bargraph`, so our initial
bundle definition looks like:
```js
```diff
define([
'openmct'
], function (
@@ -1374,7 +1373,7 @@ define([
{
"name": "Bar Graph",
"key": "example.bargraph",
"cssClass": "icon-autoflow-tabular",
"cssclass": "icon-autoflow-tabular",
"templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ],
"delegation": true
@@ -1407,7 +1406,7 @@ For this tutorial, we'll assume that we've sketched out our template and CSS
file ahead of time to describe the general look we want for the view. These
look like:
```html
```diff
<div class="example-bargraph">
<div class="example-tick-labels">
<div class="example-tick-label" style="bottom: 0%">High</div>
@@ -1458,7 +1457,7 @@ bar corresponds to which telemetry point. Inline `style` attributes are used
wherever dynamic positioning (handled by a script) is anticipated.
The corresponding CSS file which styles and positions these elements:
```css
```diff
.example-bargraph {
position: absolute;
top: 0;
@@ -1556,7 +1555,7 @@ Notably, we will not try to show telemetry data after this step.
To support this, we will add a new controller which supports our Bar Graph view:
```js
```diff
define(function () {
function BarGraphController($scope, telemetryHandler) {
var handle;
@@ -1608,7 +1607,7 @@ telemetry objects in view, as well as the width for each bar.
We will also utilize this from our template:
```html
```diff
+ <div class="example-bargraph" ng-controller="BarGraphController">
<div class="example-tick-labels">
+ <div ng-repeat="value in [low, middle, high] track by $index"
@@ -1661,7 +1660,7 @@ Finally, we expose our controller from our bundle definition. Note that the
depends declaration includes both `$scope` as well as the `telemetryHandler`
service we made use of.
```js
```diff
define([
'openmct',
'./src/controllers/BarGraphController'
@@ -1677,7 +1676,7 @@ define([
{
"name": "Bar Graph",
"key": "example.bargraph",
"cssClass": "icon-autoflow-tabular",
"cssclass": "icon-autoflow-tabular",
"templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ],
"delegation": true
@@ -1716,7 +1715,7 @@ First, let's add expose some more functionality from our controller. To make it
simple, we'll expose the top and bottom for a bar graph for a given
telemetry-providing domain object, as percentages.
```js
```diff
define(function () {
function BarGraphController($scope, telemetryHandler) {
var handle;
@@ -1768,7 +1767,7 @@ decide this.
Next, we utilize this functionality from the template:
```html
```diff
<div class="example-bargraph" ng-controller="BarGraphController">
<div class="example-tick-labels">
<div ng-repeat="value in [low, middle, high] track by $index"
@@ -1827,7 +1826,7 @@ when we return to our view later, those changes will be persisted.
First, let's add a tool bar for changing these three values in Edit mode:
```js
```diff
define([
'openmct',
'./src/controllers/BarGraphController'
@@ -1843,7 +1842,7 @@ define([
{
"name": "Bar Graph",
"key": "example.bargraph",
"cssClass": "icon-autoflow-tabular",
"cssclass": "icon-autoflow-tabular",
"templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ],
"delegation": true,
@@ -1901,7 +1900,7 @@ a view proxy to work from. We will add this to our controller, and additionally
will start reading/writing those properties to the view's `configuration`
object.
```js
```diff
define(function () {
function BarGraphController($scope, telemetryHandler) {
var handle;
@@ -2024,7 +2023,7 @@ For purposes of this tutorial, a simple node server is provided to stand
in place of this existing telemetry system. It generates real-time data
and exposes it over a WebSocket connection.
```js
```diff
/*global require,process,console*/
var CONFIG = {
@@ -2206,7 +2205,7 @@ used by the server. It uses a custom format and, for purposes of example,
contains three "subsystems" containing a mix of numeric and string-based
telemetry.
```json
```diff
{
"name": "Example Spacecraft",
"identifier": "sc",
@@ -2320,7 +2319,7 @@ define([
{
"name": "Spacecraft",
"key": "example.spacecraft",
"cssClass": "icon-object"
"cssclass": "icon-object"
}
],
"roots": [
@@ -2433,7 +2432,7 @@ server. Our first step will be to add a service that will handle interactions
with the server; this will not be used by Open MCT directly, but will be
used by subsequent components we add.
```js
```diff
/*global define,WebSocket*/
define(
@@ -2488,7 +2487,7 @@ subsystems. This means that we need to convert the data from the dictionary
into domain object models, and expose these to Open MCT via a
`modelService`.
```js
```diff
/*global define*/
define(
@@ -2622,7 +2621,7 @@ This allows our telemetry dictionary to be expressed as domain object models
fix this, we will need another script which will add these subsystems to the
root-level object we added in Step 1.
```js
```diff
/*global define*/
define(
@@ -2687,7 +2686,7 @@ Finally, we wire in these changes by modifying our plugin's `bundle.js` to
provide metadata about how these pieces interact (both with each other, and
with the platform):
```js
```diff
define([
'openmct',
+ './src/ExampleTelemetryServerAdapter',
@@ -2706,18 +2705,18 @@ define([
{
"name": "Spacecraft",
"key": "example.spacecraft",
"cssClass": "icon-object"
"cssclass": "icon-object"
},
+ {
+ "name": "Subsystem",
+ "key": "example.subsystem",
+ "cssClass": "icon-object",
+ "cssclass": "icon-object",
+ "model": { "composition": [] }
+ },
+ {
+ "name": "Measurement",
+ "key": "example.measurement",
+ "cssClass": "icon-telemetry",
+ "cssclass": "icon-telemetry",
+ "model": { "telemetry": {} },
+ "telemetry": {
+ "source": "example.source",
@@ -2835,7 +2834,7 @@ will do so for the server's historical telemetry.
Our first step will be to add a method to our server adapter which allows us to
send history requests to the server:
```js
```diff
/*global define,WebSocket*/
define(
@@ -2894,7 +2893,7 @@ identifier, the pending promise is resolved.
This `history` method will be used by a `telemetryService` provider which we
will implement:
```js
```diff
/*global define*/
define(
@@ -2980,7 +2979,7 @@ Finally, note that we also have a `subscribe` method, to satisfy the interface o
This script uses an `ExampleTelemetrySeries` class, which looks like:
```js
```diff
/*global define*/
define(
@@ -3012,7 +3011,7 @@ it with the interface expected by the platform (the methods shown.)
Finally, we expose this `telemetryService` provider declaratively:
```js
```diff
define([
'openmct',
'./src/ExampleTelemetryServerAdapter',
@@ -3031,18 +3030,18 @@ define([
{
"name": "Spacecraft",
"key": "example.spacecraft",
"cssClass": "icon-object"
"cssclass": "icon-object"
},
{
"name": "Subsystem",
"key": "example.subsystem",
"cssClass": "icon-object",
"cssclass": "icon-object",
"model": { "composition": [] }
},
{
"name": "Measurement",
"key": "example.measurement",
"cssClass": "icon-telemetry",
"cssclass": "icon-telemetry",
"model": { "telemetry": {} },
"telemetry": {
"source": "example.source",
@@ -3127,7 +3126,7 @@ Finally, we want to utilize the server's ability to subscribe to telemetry
from Open MCT. To do this, first we want to expose some new methods for
this from our server adapter:
```js
```diff
/*global define,WebSocket*/
define(
@@ -3200,7 +3199,7 @@ with these subscriptions.
We then need only to utilize these methods from our `telemetryService`:
```js
```diff
/*global define*/
define(
@@ -3306,4 +3305,4 @@ server can handle this.)
Running Open MCT again, we can still plot our historical telemetry - but
now we also see that it updates in real-time as more data comes in from the
server.

View File

@@ -49,7 +49,7 @@ define([
{
"key": "eventGenerator",
"name": "Event Message Generator",
"cssClass": "icon-folder-new",
"cssclass": "icon-folder-new",
"description": "For development use. Creates sample event message data that mimics a live data stream.",
"priority": 10,
"features": "creation",

View File

@@ -36,7 +36,7 @@ define([
"name": "Export Telemetry as CSV",
"implementation": ExportTelemetryAsCSVAction,
"category": "contextual",
"cssClass": "icon-download",
"cssclass": "icon-download",
"depends": [ "exportService" ]
}
]

183
example/generator/bundle.js Normal file
View File

@@ -0,0 +1,183 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
"./src/SinewaveTelemetryProvider",
"./src/SinewaveLimitCapability",
"./src/SinewaveDeltaFormat",
'legacyRegistry'
], function (
SinewaveTelemetryProvider,
SinewaveLimitCapability,
SinewaveDeltaFormat,
legacyRegistry
) {
"use strict";
legacyRegistry.register("example/generator", {
"name": "Sine Wave Generator",
"description": "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
"extensions": {
"components": [
{
"implementation": SinewaveTelemetryProvider,
"type": "provider",
"provides": "telemetryService",
"depends": [
"$q",
"$timeout"
]
}
],
"capabilities": [
{
"key": "limit",
"implementation": SinewaveLimitCapability
}
],
"formats": [
{
"key": "example.delta",
"implementation": SinewaveDeltaFormat
}
],
"constants": [
{
"key": "TIME_CONDUCTOR_DOMAINS",
"value": [
{
"key": "time",
"name": "Time"
},
{
"key": "yesterday",
"name": "Yesterday"
},
{
"key": "delta",
"name": "Delta",
"format": "example.delta"
}
],
"priority": -1
}
],
"types": [
{
"key": "generator",
"name": "Sine Wave Generator",
"cssclass": "icon-telemetry",
"description": "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
"priority": 10,
"features": "creation",
"model": {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1
}
},
"telemetry": {
"source": "generator",
"domains": [
{
"key": "utc",
"name": "Time"
},
{
"key": "yesterday",
"name": "Yesterday"
},
{
"key": "delta",
"name": "Delta",
"format": "example.delta"
}
],
"ranges": [
{
"key": "sin",
"name": "Sine"
},
{
"key": "cos",
"name": "Cosine"
}
]
},
"properties": [
{
"name": "Period",
"control": "textfield",
"cssclass": "l-input-sm l-numeric",
"key": "period",
"required": true,
"property": [
"telemetry",
"period"
],
"pattern": "^\\d*(\\.\\d*)?$"
},
{
"name": "Amplitude",
"control": "textfield",
"cssclass": "l-input-sm l-numeric",
"key": "amplitude",
"required": true,
"property": [
"telemetry",
"amplitude"
],
"pattern": "^\\d*(\\.\\d*)?$"
},
{
"name": "Offset",
"control": "textfield",
"cssclass": "l-input-sm l-numeric",
"key": "offset",
"required": true,
"property": [
"telemetry",
"offset"
],
"pattern": "^\\d*(\\.\\d*)?$"
},
{
"name": "Data Rate (hz)",
"control": "textfield",
"cssclass": "l-input-sm l-numeric",
"key": "dataRateInHz",
"required": true,
"property": [
"telemetry",
"dataRateInHz"
],
"pattern": "^\\d*(\\.\\d*)?$"
}
]
}
]
}
});
});

View File

@@ -1,171 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
"./GeneratorProvider",
"./SinewaveLimitCapability",
"./SinewaveDeltaFormat"
], function (
GeneratorProvider,
SinewaveLimitCapability,
SinewaveDeltaFormat
) {
var legacyExtensions = {
"capabilities": [
{
"key": "limit",
"implementation": SinewaveLimitCapability
}
],
"formats": [
{
"key": "example.delta",
"implementation": SinewaveDeltaFormat
}
],
"constants": [
{
"key": "TIME_CONDUCTOR_DOMAINS",
"value": [
{
"key": "time",
"name": "Time"
},
{
"key": "yesterday",
"name": "Yesterday"
},
{
"key": "delta",
"name": "Delta"
}
],
"priority": -1
}
]
}
return function(openmct){
//Register legacy extensions for things not yet supported by the new API
Object.keys(legacyExtensions).forEach(function (type){
var extensionsOfType = legacyExtensions[type];
extensionsOfType.forEach(function (extension) {
openmct.legacyExtension(type, extension)
})
});
openmct.types.addType("generator", {
label: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
cssClass: "icon-telemetry",
creatable: true,
form: [
{
name: "Period",
control: "textfield",
cssClass: "l-input-sm l-numeric",
key: "period",
required: true,
property: [
"telemetry",
"period"
],
pattern: "^\\d*(\\.\\d*)?$"
},
{
name: "Amplitude",
control: "textfield",
cssClass: "l-input-sm l-numeric",
key: "amplitude",
required: true,
property: [
"telemetry",
"amplitude"
],
pattern: "^\\d*(\\.\\d*)?$"
},
{
name: "Offset",
control: "textfield",
cssClass: "l-input-sm l-numeric",
key: "offset",
required: true,
property: [
"telemetry",
"offset"
],
pattern: "^\\d*(\\.\\d*)?$"
},
{
name: "Data Rate (hz)",
control: "textfield",
cssClass: "l-input-sm l-numeric",
key: "dataRateInHz",
required: true,
property: [
"telemetry",
"dataRateInHz"
],
pattern: "^\\d*(\\.\\d*)?$"
}
],
initialize: function (object) {
object.telemetry = {
period: 10,
amplitude: 1,
offset: 0,
dataRateInHz: 1,
domains: [
{
key: "utc",
name: "Time",
format: "utc"
},
{
key: "yesterday",
name: "Yesterday",
format: "utc"
},
{
key: "delta",
name: "Delta",
format: "example.delta"
}
],
ranges: [
{
key: "sin",
name: "Sine"
},
{
key: "cos",
name: "Cosine"
}
]
};
}
});
openmct.telemetry.addProvider(new GeneratorProvider());
};
});

View File

@@ -41,10 +41,6 @@ define([
return domainObject.type === 'generator';
};
GeneratorProvider.prototype.supportsRequest =
GeneratorProvider.prototype.supportsSubscribe =
GeneratorProvider.prototype.canProvideTelemetry;
GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request) {
var props = [
'amplitude',

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
/*global define,Promise*/
define({
START_TIME: Date.now() - 24 * 60 * 60 * 1000 // Now minus a day.

View File

@@ -19,11 +19,12 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
/*global define,Promise*/
define(
['./SinewaveConstants', 'moment'],
function (SinewaveConstants, moment) {
"use strict";
var START_TIME = SinewaveConstants.START_TIME,
FORMAT_REGEX = /^-?\d+:\d+:\d+$/,

View File

@@ -24,7 +24,6 @@
(function () {
var FIFTEEN_MINUTES = 15 * 60 * 1000;
var handlers = {
subscribe: onSubscribe,
@@ -52,7 +51,6 @@
function onSubscribe(message) {
var data = message.data;
// Keep
var start = Date.now();
var step = 1000 / data.dataRateInHz;
var nextStep = start - (start % step) + step;
@@ -84,11 +82,8 @@
function onRequest(message) {
var data = message.data;
if (data.end == undefined) {
data.end = Date.now();
}
if (data.start == undefined){
data.start = data.end - FIFTEEN_MINUTES;
if (!data.start || !data.end) {
throw new Error('missing start and end!');
}
var now = Date.now();

View File

@@ -49,7 +49,7 @@ define([
{
"key": "imagery",
"name": "Example Imagery",
"cssClass": "icon-image",
"cssclass": "icon-image",
"features": "creation",
"description": "For development use. Creates example imagery data that mimics a live imagery stream.",
"priority": 10,

View File

@@ -31,7 +31,7 @@ define(['../../../platform/features/conductor/core/src/timeSystems/LocalClock'],
this.metadata = {
key: 'test-lad',
mode: 'lad',
cssClass: 'icon-clock',
cssclass: 'icon-clock',
label: 'Latest Available Data',
name: 'Latest available data',
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'

View File

@@ -37,79 +37,74 @@ define([
legacyRegistry.register("example/msl", {
"name" : "Mars Science Laboratory Data Adapter",
"extensions" : {
"types": [
{
"name":"Mars Science Laboratory",
"key": "msl.curiosity",
"cssClass": "icon-object"
},
{
"name": "Instrument",
"key": "msl.instrument",
"cssClass": "icon-object",
"model": {"composition": []}
},
{
"name": "Measurement",
"key": "msl.measurement",
"cssClass": "icon-telemetry",
"model": {"telemetry": {}},
"telemetry": {
"source": "rems.source",
"domains": [
{
"name": "Time",
"key": "utc",
"format": "utc"
}
]
}
"types": [
{
"name":"Mars Science Laboratory",
"key": "msl.curiosity",
"cssclass": "icon-object"
},
{
"name": "Instrument",
"key": "msl.instrument",
"cssclass": "icon-object",
"model": {"composition": []}
},
{
"name": "Measurement",
"key": "msl.measurement",
"cssclass": "icon-telemetry",
"model": {"telemetry": {}},
"telemetry": {
"source": "rems.source",
"domains": [
{
"name": "Time",
"key": "utc",
"format": "utc"
}
]
}
],
"constants": [
{
"key": "REMS_WS_URL",
"value": "/proxyUrl?url=http://cab.inta-csic.es/rems/wp-content/plugins/marsweather-widget/api.php"
}
],
"constants": [
{
"key": "REMS_WS_URL",
"value": "/proxyUrl?url=http://cab.inta-csic.es/rems/wp-content/plugins/marsweather-widget/api.php"
}
],
"roots": [
{
"id": "msl:curiosity",
"priority" : "preferred",
"model": {
"type": "msl.curiosity",
"name": "Mars Science Laboratory",
"composition": ["msl_tlm:rems"]
}
],
"roots": [
{
"id": "msl:curiosity"
}
],
"models": [
{
"id": "msl:curiosity",
"priority": "preferred",
"model": {
"type": "msl.curiosity",
"name": "Mars Science Laboratory",
"composition": ["msl_tlm:rems"]
}
}
],
"services": [
{
"key":"rems.adapter",
"implementation": RemsTelemetryServerAdapter,
"depends": ["$q", "$http", "$log", "REMS_WS_URL"]
}
],
"components": [
{
"provides": "modelService",
"type": "provider",
"implementation": RemsTelemetryModelProvider,
"depends": ["rems.adapter"]
},
{
"provides": "telemetryService",
"type": "provider",
"implementation": RemsTelemetryProvider,
"depends": ["rems.adapter", "$q"]
}
]
}
}
],
"services": [
{
"key":"rems.adapter",
"implementation": RemsTelemetryServerAdapter,
"depends": ["$q", "$http", "$log", "REMS_WS_URL"]
}
],
"components": [
{
"provides": "modelService",
"type": "provider",
"implementation": RemsTelemetryModelProvider,
"depends": ["rems.adapter"]
},
{
"provides": "telemetryService",
"type": "provider",
"implementation": RemsTelemetryProvider,
"depends": ["rems.adapter", "$q"]
}
]
}
});
});

View File

@@ -81,7 +81,7 @@ define([
{
"key": "plot",
"name": "Example Telemetry Plot",
"cssClass": "icon-telemetry-panel",
"cssclass": "icon-telemetry-panel",
"description": "For development use. A plot for displaying telemetry.",
"priority": 10,
"delegates": [
@@ -129,7 +129,7 @@ define([
{
"name": "Period",
"control": "textfield",
"cssClass": "l-input-sm l-numeric",
"cssclass": "l-input-sm l-numeric",
"key": "period",
"required": true,
"property": [

View File

@@ -63,7 +63,7 @@ define(
* Get the CSS class that defines the icon
* to display in this indicator. This will appear
* as a dataflow icon.
* @returns {string} the cssClass of the dataflow icon
* @returns {string} the cssclass of the dataflow icon
*/
getCssClass: function () {
return "icon-connectivity";

View File

@@ -33,11 +33,6 @@ define([
legacyRegistry.register("example/scratchpad", {
"extensions": {
"roots": [
{
"id": "scratch:root"
}
],
"models": [
{
"id": "scratch:root",
"model": {

View File

@@ -35,11 +35,6 @@ define([
"description": "Example illustrating the addition of a static top-level hierarchy",
"extensions": {
"roots": [
{
"id": "exampleTaxonomy"
}
],
"models": [
{
"id": "exampleTaxonomy",
"model": {

View File

@@ -69,11 +69,6 @@ var gulp = require('gulp'),
}
};
if (process.env.NODE_ENV === 'development') {
options.requirejsOptimize.optimize = 'none';
}
gulp.task('scripts', function () {
var requirejsOptimize = require('gulp-requirejs-optimize');
var replace = require('gulp-replace-task');

View File

@@ -25,32 +25,19 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title></title>
<script src="openmct-tutorial/lib/http.js">
</script>
<script src="bower_components/requirejs/require.js">
</script>
<script src="openmct-tutorial/dictionary-plugin.js">
</script>
<script src="openmct-tutorial/realtime-telemetry-plugin.js">
</script>
<script src="openmct-tutorial/historical-telemetry-plugin.js">
</script>
<script>
require(['openmct'], function (openmct) {
[
'example/imagery',
'example/eventGenerator'
'example/eventGenerator',
'example/generator',
'platform/features/my-items',
'platform/persistence/local'
].forEach(
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
);
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.Generator());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(DictionaryPlugin());
openmct.install(RealtimeTelemetryPlugin());
openmct.install(HistoricalTelemetryPlugin());
openmct.start();
});
</script>

View File

@@ -76,7 +76,7 @@ module.exports = function(config) {
// Specify browsers to run tests in.
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: [
'Chrome'
'PhantomJS'
],
// Code coverage reporting.

Submodule openmct-tutorial deleted from 7076a15d3a

View File

@@ -84,11 +84,5 @@ define([
return new Main().run(defaultRegistry);
});
// For now, install conductor by default
openmct.install(openmct.plugins.Conductor({
showConductor: false
}));
return openmct;
});

View File

@@ -25,12 +25,13 @@
"jsdoc": "^3.3.2",
"jshint": "^2.7.0",
"karma": "^0.13.3",
"karma-chrome-launcher": "^0.1.12",
"karma-chrome-launcher": "^0.1.8",
"karma-cli": "0.0.4",
"karma-coverage": "^0.5.3",
"karma-html-reporter": "^0.2.7",
"karma-jasmine": "^0.1.5",
"karma-junit-reporter": "^0.3.8",
"karma-phantomjs-launcher": "^1.0.0",
"karma-requirejs": "^0.2.2",
"lodash": "^3.10.1",
"markdown-toc": "^0.11.7",
@@ -39,6 +40,7 @@
"mkdirp": "^0.5.1",
"moment": "^2.11.1",
"node-bourbon": "^4.2.3",
"phantomjs-prebuilt": "2.1.11 || >2.1.12 <3.0.0",
"requirejs": "2.1.x",
"split": "^1.0.0"
},

View File

@@ -41,6 +41,7 @@ define([
"text!./res/templates/items/items.html",
"text!./res/templates/browse/object-properties.html",
"text!./res/templates/browse/inspector-region.html",
"text!./res/templates/view-object.html",
'legacyRegistry'
], function (
BrowseController,
@@ -63,6 +64,7 @@ define([
itemsTemplate,
objectPropertiesTemplate,
inspectorRegionTemplate,
viewObjectTemplate,
legacyRegistry
) {
@@ -139,6 +141,10 @@ define([
}
],
"representations": [
{
"key": "view-object",
"template": viewObjectTemplate
},
{
"key": "browse-object",
"template": browseObjectTemplate,
@@ -198,10 +204,7 @@ define([
"services": [
{
"key": "navigationService",
"implementation": NavigationService,
"depends": [
"$window"
]
"implementation": NavigationService
}
],
"actions": [
@@ -209,7 +212,10 @@ define([
"key": "navigate",
"implementation": NavigateAction,
"depends": [
"navigationService"
"navigationService",
"$q",
"policyService",
"$window"
]
},
{
@@ -226,7 +232,7 @@ define([
"$window"
],
"group": "windowing",
"cssClass": "icon-new-window",
"cssclass": "icon-new-window",
"priority": "preferred"
},
{
@@ -241,7 +247,7 @@ define([
{
"key": "items",
"name": "Items",
"cssClass": "icon-thumbs-strip",
"cssclass": "icon-thumbs-strip",
"description": "Grid of available items",
"template": itemsTemplate,
"uses": [

View File

@@ -63,7 +63,7 @@
<mct-split-pane class='l-object-and-inspector contents abs' anchor='right'>
<div class='split-pane-component t-object pane primary-pane left'>
<mct-representation mct-object="navigatedObject"
key="navigatedObject.getCapability('status').get('editing') ? 'edit-object' : 'browse-object'"
key="'view-object'"
class="abs holder holder-object">
</mct-representation>
<a class="mini-tab-icon anchor-right mobile-hide toggle-pane toggle-inspect flush-right"

View File

@@ -0,0 +1,33 @@
<!--
Open MCT, Copyright (c) 2014-2016, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<!--
A representation that allows the 'View' region of an object view to change
dynamically (eg. between browse and edit modes). Values correspond to a
representation key, and currently defaults to 'browse-object'.
In the case of edit, the EditRepresenter will change this to editable
representation of the object as needed.
-->
<mct-representation mct-object="domainObject"
key="viewObjectTemplate || 'browse-object'"
class="abs holder">
</mct-representation>

View File

@@ -48,16 +48,11 @@ define(
defaultPath
) {
var initialPath = ($route.current.params.ids || defaultPath).split("/");
var currentIds;
var currentIds = $route.current.params.ids;
$scope.treeModel = {
selectedObject: undefined,
onSelection: function (object) {
navigationService.setNavigation(object, true);
},
allowSelection: function (object) {
return navigationService.shouldNavigate();
}
selectedObject: undefined
};
function idsForObject(domainObject) {
@@ -108,6 +103,7 @@ define(
function navigateToObject(desiredObject) {
$scope.navigatedObject = desiredObject;
$scope.treeModel.selectedObject = desiredObject;
navigationService.setNavigation(desiredObject);
currentIds = idsForObject(desiredObject);
$route.current.pathParams.ids = currentIds;
$location.path('/browse/' + currentIds);
@@ -118,11 +114,10 @@ define(
.then(function (root) {
return findViaComposition(root, path);
})
.then(function (object) {
navigationService.setNavigation(object);
});
.then(navigateToObject);
}
getObject('ROOT')
.then(function (root) {
$scope.domainObject = root;
@@ -142,6 +137,15 @@ define(
// Listen for changes in navigation state.
navigationService.addListener(navigateDirectlyToModel);
// Also listen for changes which come from the tree. Changes in
// the tree will trigger a change in browse navigation state.
$scope.$watch("treeModel.selectedObject", function (newObject, oldObject) {
if (oldObject !== newObject) {
navigateDirectlyToModel(newObject);
}
});
// Listen for route changes which are caused by browser events
// (e.g. bookmarks to pages in OpenMCT) and prevent them. Instead,
// navigate to the path ourselves, which results in it being

View File

@@ -33,9 +33,12 @@ define(
* @constructor
* @implements {Action}
*/
function NavigateAction(navigationService, context) {
function NavigateAction(navigationService, $q, policyService, $window, context) {
this.domainObject = context.domainObject;
this.$q = $q;
this.navigationService = navigationService;
this.policyService = policyService;
this.$window = $window;
}
/**
@@ -44,12 +47,36 @@ define(
* navigation has been updated
*/
NavigateAction.prototype.perform = function () {
if (this.navigationService.shouldNavigate()) {
this.navigationService.setNavigation(this.domainObject, true);
return Promise.resolve({});
var self = this,
navigateTo = this.domainObject,
currentObject = self.navigationService.getNavigation();
function allow() {
var navigationAllowed = true;
self.policyService.allow("navigation", currentObject, navigateTo, function (message) {
navigationAllowed = self.$window.confirm(message + "\r\n\r\n" +
" Are you sure you want to continue?");
});
return navigationAllowed;
}
function cancelIfEditing() {
var editing = currentObject.hasCapability('editor') &&
currentObject.getCapability('editor').isEditContextRoot();
return self.$q.when(editing && currentObject.getCapability("editor").finish());
}
function navigate() {
return self.navigationService.setNavigation(navigateTo);
}
if (allow()) {
return cancelIfEditing().then(navigate);
} else {
return this.$q.when(false);
}
return Promise.reject('Navigation Prevented by User');
};
/**

View File

@@ -30,23 +30,16 @@ define(
/**
* The navigation service maintains the application's current
* navigation state, and allows listening for changes thereto.
*
* @memberof platform/commonUI/browse
* @constructor
*/
function NavigationService($window) {
function NavigationService() {
this.navigated = undefined;
this.callbacks = [];
this.checks = [];
this.$window = $window;
this.oldUnload = $window.onbeforeunload;
$window.onbeforeunload = this.onBeforeUnload.bind(this);
}
/**
* Get the current navigation state.
*
* @returns {DomainObject} the object that is navigated-to
*/
NavigationService.prototype.getNavigation = function () {
@@ -54,33 +47,16 @@ define(
};
/**
* Navigate to a specified object. If navigation checks exist and
* return reasons to prevent navigation, it will prompt the user before
* continuing. Trying to navigate to the currently navigated object will
* do nothing.
*
* If a truthy value is passed for `force`, it will skip navigation
* and will not prevent navigation to an already selected object.
*
* Set the current navigation state. This will invoke listeners.
* @param {DomainObject} domainObject the domain object to navigate to
* @param {Boolean} force if true, force navigation to occur.
* @returns {Boolean} true if navigation occured, otherwise false.
*/
NavigationService.prototype.setNavigation = function (domainObject, force) {
if (force) {
this.doNavigation(domainObject);
return true;
NavigationService.prototype.setNavigation = function (value) {
if (this.navigated !== value) {
this.navigated = value;
this.callbacks.forEach(function (callback) {
callback(value);
});
}
if (this.navigated === domainObject) {
return true;
}
var doNotNavigate = this.shouldWarnBeforeNavigate();
if (doNotNavigate && !this.$window.confirm(doNotNavigate)) {
return false;
}
this.doNavigation(domainObject);
return true;
};
@@ -88,7 +64,6 @@ define(
* Listen for changes in navigation. The passed callback will
* be invoked with the new domain object of navigation when
* this changes.
*
* @param {function} callback the callback to invoke when
* navigation state changes
*/
@@ -98,7 +73,6 @@ define(
/**
* Stop listening for changes in navigation state.
*
* @param {function} callback the callback which should
* no longer be invoked when navigation state
* changes
@@ -109,89 +83,6 @@ define(
});
};
/**
* Check if navigation should proceed. May prompt a user for input
* if any checkFns return messages. Returns true if the user wishes to
* navigate, otherwise false. If using this prior to calling
* `setNavigation`, you should call `setNavigation` with `force=true`
* to prevent duplicate dialogs being displayed to the user.
*
* @returns {Boolean} true if the user wishes to navigate, otherwise false.
*/
NavigationService.prototype.shouldNavigate = function () {
var doNotNavigate = this.shouldWarnBeforeNavigate();
return !doNotNavigate || this.$window.confirm(doNotNavigate);
};
/**
* Register a check function to be called before any navigation occurs.
* Check functions should return a human readable "message" if
* there are any reasons to prevent navigation. Otherwise, they should
* return falsy. Returns a function which can be called to remove the
* check function.
*
* @param {Function} checkFn a function to call before navigation occurs.
* @returns {Function} removeCheck call to remove check
*/
NavigationService.prototype.checkBeforeNavigation = function (checkFn) {
this.checks.push(checkFn);
return function removeCheck() {
this.checks = this.checks.filter(function (fn) {
return checkFn !== fn;
});
}.bind(this);
};
/**
* Private method to actually perform navigation.
*
* @private
*/
NavigationService.prototype.doNavigation = function (value) {
this.navigated = value;
this.callbacks.forEach(function (callback) {
callback(value);
});
};
/**
* Returns either a false value, or a string that should be displayed
* to the user before navigation is allowed.
*
* @private
*/
NavigationService.prototype.shouldWarnBeforeNavigate = function () {
var reasons = [];
this.checks.forEach(function (checkFn) {
var reason = checkFn();
if (reason) {
reasons.push(reason);
}
});
if (reasons.length) {
return reasons.join('\n');
}
return false;
};
/**
* Listener for window on before unload event-- will warn before
* navigation is allowed.
*
* @private
*/
NavigationService.prototype.onBeforeUnload = function () {
var shouldWarnBeforeNavigate = this.shouldWarnBeforeNavigate();
if (shouldWarnBeforeNavigate) {
return shouldWarnBeforeNavigate;
}
if (this.oldUnload) {
return this.oldUnload.apply(undefined, [].slice.apply(arguments));
}
};
return NavigationService;
}
);

View File

@@ -43,24 +43,24 @@ define([], function () {
return context.getParent();
}
function preventOrphanNavigation(domainObject) {
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);
parent.useCapability('composition')
.then(function (composees) {
var isOrphan = composees.every(function (c) {
return c.getId() !== domainObject.getId();
});
if (isOrphan) {
parent.getCapability('action').perform('navigate');
}
});
return parent.getCapability('action').perform('navigate');
}
function checkNavigation() {
var navigatedObject = navigationService.getNavigation();
if (navigatedObject.hasCapability('context')) {
if (navigatedObject.hasCapability('context') &&
isOrphan(navigatedObject)) {
if (!navigatedObject.getCapability('editor').isEditContextRoot()) {
preventOrphanNavigation(navigatedObject);
navigateToParent(navigatedObject);
}
}
}

View File

@@ -46,12 +46,12 @@ define(
};
FullscreenAction.prototype.getMetadata = function () {
// We override getMetadata, because the icon cssClass and
// We override getMetadata, because the icon cssclass and
// description need to be determined at run-time
// based on whether or not we are currently
// full screen.
var metadata = Object.create(FullscreenAction);
metadata.cssClass = screenfull.isFullscreen ? "icon-fullscreen-expand" : "icon-fullscreen-collapse";
metadata.cssclass = screenfull.isFullscreen ? "icon-fullscreen-expand" : "icon-fullscreen-collapse";
metadata.description = screenfull.isFullscreen ?
EXIT_FULLSCREEN : ENTER_FULLSCREEN;
metadata.group = "windowing";

View File

@@ -24,14 +24,8 @@
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
*/
define(
[
"../src/BrowseController",
"../src/navigation/NavigationService"
],
function (
BrowseController,
NavigationService
) {
["../src/BrowseController"],
function (BrowseController) {
describe("The browse controller", function () {
var mockScope,
@@ -50,7 +44,7 @@ define(
function waitsForNavigation() {
var calls = mockNavigationService.setNavigation.calls.length;
waitsFor(function () {
return mockNavigationService.setNavigation.calls.length > calls;
return mockNavigationService.setNavigation.calls.length > calls ;
});
}
@@ -98,16 +92,15 @@ define(
"objectService",
["getObjects"]
);
mockNavigationService = new NavigationService({});
[
"getNavigation",
"setNavigation",
"addListener",
"removeListener"
].forEach(function (method) {
spyOn(mockNavigationService, method)
.andCallThrough();
});
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[
"getNavigation",
"setNavigation",
"addListener",
"removeListener"
]
);
mockRootObject = jasmine.createSpyObj(
"rootObjectContainer",
["getId", "getCapability", "getModel", "useCapability", "hasCapability"]

View File

@@ -23,74 +23,145 @@
/**
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
*/
define([
"../../src/navigation/NavigateAction"
], function (
NavigateAction
) {
define(
["../../src/navigation/NavigateAction"],
function (NavigateAction) {
describe("The navigate action", function () {
var mockNavigationService,
mockDomainObject,
action;
describe("The navigate action", function () {
var mockNavigationService,
mockQ,
mockDomainObject,
mockPolicyService,
mockNavigatedObject,
mockWindow,
capabilities,
action;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
function waitForCall() {
var called = false;
waitsFor(function () {
return called;
beforeEach(function () {
capabilities = {};
mockQ = { when: mockPromise };
mockNavigatedObject = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel",
"hasCapability",
"getCapability"
]
);
capabilities.editor = jasmine.createSpyObj("editorCapability", [
"isEditContextRoot",
"finish"
]);
mockNavigatedObject.getCapability.andCallFake(function (capability) {
return capabilities[capability];
});
mockNavigatedObject.hasCapability.andReturn(false);
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[
"setNavigation",
"getNavigation"
]
);
mockNavigationService.getNavigation.andReturn(mockNavigatedObject);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel"
]
);
mockPolicyService = jasmine.createSpyObj("policyService",
[
"allow"
]);
mockWindow = jasmine.createSpyObj("$window",
[
"confirm"
]);
action = new NavigateAction(
mockNavigationService,
mockQ,
mockPolicyService,
mockWindow,
{ domainObject: mockDomainObject }
);
});
return function () {
called = true;
};
}
beforeEach(function () {
mockNavigationService = jasmine.createSpyObj(
"navigationService",
[
"shouldNavigate",
"setNavigation"
]
);
mockDomainObject = {};
action = new NavigateAction(
mockNavigationService,
{ domainObject: mockDomainObject }
);
});
it("sets navigation if it is allowed", function () {
mockNavigationService.shouldNavigate.andReturn(true);
action.perform()
.then(waitForCall());
runs(function () {
expect(mockNavigationService.setNavigation)
.toHaveBeenCalledWith(mockDomainObject, true);
it("invokes the policy service to determine if navigation" +
" allowed", function () {
action.perform();
expect(mockPolicyService.allow)
.toHaveBeenCalledWith("navigation", jasmine.any(Object), jasmine.any(Object), jasmine.any(Function));
});
});
it("does not set navigation if it is not allowed", function () {
mockNavigationService.shouldNavigate.andReturn(false);
var onSuccess = jasmine.createSpy('onSuccess');
action.perform()
.then(onSuccess, waitForCall());
runs(function () {
expect(onSuccess).not.toHaveBeenCalled();
expect(mockNavigationService.setNavigation)
.not
.toHaveBeenCalledWith(mockDomainObject);
it("prompts user if policy rejection", function () {
action.perform();
expect(mockPolicyService.allow).toHaveBeenCalled();
mockPolicyService.allow.mostRecentCall.args[3]();
expect(mockWindow.confirm).toHaveBeenCalled();
});
});
it("is only applicable when a domain object is in context", function () {
expect(NavigateAction.appliesTo({})).toBeFalsy();
expect(NavigateAction.appliesTo({
domainObject: mockDomainObject
})).toBeTruthy();
});
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();
});
});
describe("in edit mode", function () {
beforeEach(function () {
mockNavigatedObject.hasCapability.andCallFake(function (capability) {
return capability === "editor";
});
capabilities.editor.isEditContextRoot.andReturn(true);
});
it("finishes editing if in edit mode", function () {
action.perform();
expect(capabilities.editor.finish)
.toHaveBeenCalled();
});
});
it("is only applicable when a domain object is in context", function () {
expect(NavigateAction.appliesTo({})).toBeFalsy();
expect(NavigateAction.appliesTo({
domainObject: mockDomainObject
})).toBeTruthy();
});
});
}
);

View File

@@ -28,12 +28,10 @@ define(
function (NavigationService) {
describe("The navigation service", function () {
var $window,
navigationService;
var navigationService;
beforeEach(function () {
$window = jasmine.createSpyObj('$window', ['confirm']);
navigationService = new NavigationService($window);
navigationService = new NavigationService();
});
it("stores navigation state", function () {

View File

@@ -33,7 +33,7 @@ define([
mockContext,
mockActionCapability,
mockEditor,
testParentComposition,
testParentModel,
testId,
mockThrottledFns;
@@ -41,6 +41,7 @@ define([
testId = 'some-identifier';
mockThrottledFns = [];
testParentModel = {};
mockTopic = jasmine.createSpy('topic');
mockThrottle = jasmine.createSpy('throttle');
@@ -54,12 +55,14 @@ define([
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getId',
'getCapability',
'getModel',
'hasCapability'
]);
mockParentObject = jasmine.createSpyObj('domainObject', [
'getId',
'getCapability',
'useCapability'
'getModel',
'hasCapability'
]);
mockContext = jasmine.createSpyObj('context', ['getParent']);
mockActionCapability = jasmine.createSpyObj('action', ['perform']);
@@ -72,7 +75,9 @@ define([
mockThrottledFns.push(mockThrottledFn);
return mockThrottledFn;
});
mockTopic.andReturn(mockMutationTopic);
mockTopic.andCallFake(function (k) {
return k === 'mutation' && mockMutationTopic;
});
mockDomainObject.getId.andReturn(testId);
mockDomainObject.getCapability.andCallFake(function (c) {
return {
@@ -83,13 +88,12 @@ define([
mockDomainObject.hasCapability.andCallFake(function (c) {
return !!mockDomainObject.getCapability(c);
});
mockParentObject.getModel.andReturn(testParentModel);
mockParentObject.getCapability.andCallFake(function (c) {
return {
action: mockActionCapability
}[c];
});
testParentComposition = [];
mockParentObject.useCapability.andReturn(Promise.resolve(testParentComposition));
mockContext.getParent.andReturn(mockParentObject);
mockNavigationService.getNavigation.andReturn(mockDomainObject);
mockEditor.isEditContextRoot.andReturn(false);
@@ -122,9 +126,7 @@ define([
var prefix = isOrphan ? "" : "non-";
describe("for " + prefix + "orphan objects", function () {
beforeEach(function () {
if (!isOrphan) {
testParentComposition.push(mockDomainObject);
}
testParentModel.composition = isOrphan ? [] : [testId];
});
[false, true].forEach(function (isEditRoot) {
@@ -134,31 +136,13 @@ define([
function itNavigatesAsExpected() {
if (isOrphan && !isEditRoot) {
it("navigates to the parent", function () {
var done = false;
waitsFor(function () {
return done;
});
setTimeout(function () {
done = true;
}, 5);
runs(function () {
expect(mockActionCapability.perform)
.toHaveBeenCalledWith('navigate');
});
expect(mockActionCapability.perform)
.toHaveBeenCalledWith('navigate');
});
} else {
it("does nothing", function () {
var done = false;
waitsFor(function () {
return done;
});
setTimeout(function () {
done = true;
}, 5);
runs(function () {
expect(mockActionCapability.perform)
.not.toHaveBeenCalled();
});
expect(mockActionCapability.perform)
.not.toHaveBeenCalled();
});
}
}
@@ -173,6 +157,7 @@ define([
mockNavigationService.addListener.mostRecentCall
.args[0](mockDomainObject);
});
itNavigatesAsExpected();
});

View File

@@ -51,7 +51,7 @@ define(
});
it("provides displayable metadata", function () {
expect(action.getMetadata().cssClass).toBeDefined();
expect(action.getMetadata().cssclass).toBeDefined();
});
});

View File

@@ -53,8 +53,7 @@ define([
"depends": [
"overlayService",
"$q",
"$log",
"$document"
"$log"
]
},
{

View File

@@ -35,15 +35,11 @@ define(
* @memberof platform/commonUI/dialog
* @constructor
*/
function DialogService(overlayService, $q, $log, $document) {
function DialogService(overlayService, $q, $log) {
this.overlayService = overlayService;
this.$q = $q;
this.$log = $log;
this.activeOverlay = undefined;
this.findBody = function () {
return $document.find('body');
};
}
/**
@@ -64,8 +60,7 @@ define(
// input is asynchronous.
var deferred = this.$q.defer(),
self = this,
overlay,
handleEscKeydown;
overlay;
// Confirm function; this will be passed in to the
// overlay-dialog template and associated with a
@@ -81,22 +76,13 @@ define(
// Cancel or X button click
function cancel() {
deferred.reject();
self.findBody().off('keydown', handleEscKeydown);
self.dismissOverlay(overlay);
}
handleEscKeydown = function (event) {
if (event.keyCode === 27) {
cancel();
}
};
// Add confirm/cancel callbacks
model.confirm = confirm;
model.cancel = cancel;
this.findBody().on('keydown', handleEscKeydown);
if (this.canShowDialog(model)) {
// Add the overlay using the OverlayService, which
// will handle actual insertion into the DOM

View File

@@ -33,8 +33,6 @@ define(
mockLog,
mockOverlay,
mockDeferred,
mockDocument,
mockBody,
dialogService;
beforeEach(function () {
@@ -58,13 +56,6 @@ define(
"deferred",
["resolve", "reject"]
);
mockDocument = jasmine.createSpyObj(
"$document",
["find"]
);
mockBody = jasmine.createSpyObj('body', ['on', 'off']);
mockDocument.find.andReturn(mockBody);
mockDeferred.promise = "mock promise";
mockQ.defer.andReturn(mockDeferred);
@@ -73,8 +64,7 @@ define(
dialogService = new DialogService(
mockOverlayService,
mockQ,
mockLog,
mockDocument
mockLog
);
});
@@ -140,36 +130,6 @@ define(
);
});
it("adds a keydown event listener to the body", function () {
dialogService.getUserInput({}, {});
expect(mockDocument.find).toHaveBeenCalledWith("body");
expect(mockBody.on).toHaveBeenCalledWith("keydown", jasmine.any(Function));
});
it("destroys the event listener when the dialog is cancelled", function () {
dialogService.getUserInput({}, {});
mockOverlayService.createOverlay.mostRecentCall.args[1].cancel();
expect(mockBody.off).toHaveBeenCalledWith("keydown", jasmine.any(Function));
});
it("cancels the dialog when an escape keydown event is triggered", function () {
dialogService.getUserInput({}, {});
mockBody.on.mostRecentCall.args[1]({
keyCode: 27
});
expect(mockDeferred.reject).toHaveBeenCalled();
expect(mockDeferred.resolve).not.toHaveBeenCalled();
});
it("ignores non escape keydown events", function () {
dialogService.getUserInput({}, {});
mockBody.on.mostRecentCall.args[1]({
keyCode: 13
});
expect(mockDeferred.reject).not.toHaveBeenCalled();
expect(mockDeferred.resolve).not.toHaveBeenCalled();
});
describe("the blocking message dialog", function () {
var dialogModel = {};
var dialogHandle;

View File

@@ -2,6 +2,25 @@ Contains sources and resources associated with Edit mode.
# Extensions
## Directives
This bundle introduces the `mct-before-unload` directive, primarily for
internal use (to prompt the user to confirm navigation away from unsaved
changes in Edit mode.)
The `mct-before-unload` directive is used as an attribute whose value is
an Angular expression that is evaluated when navigation changes (either
via browser-level changes, such as the refresh button, or changes to
the Angular route, which happens when hitting the back button in Edit
mode.) The result of this evaluation, when truthy, is shown in a browser
dialog to allow the user to confirm navigation. When falsy, no prompt is
shown, allowing these dialogs to be shown conditionally. (For instance, in
Edit mode, prompts are only shown if user-initiated changes have
occurred.)
This directive may be attached to any element; its behavior will be enforced
so long as that element remains within the DOM.
# Toolbars
Views may specify the contents of a toolbar through a `toolbar`

View File

@@ -25,6 +25,7 @@ define([
"./src/controllers/EditPanesController",
"./src/controllers/ElementsController",
"./src/controllers/EditObjectController",
"./src/directives/MCTBeforeUnload",
"./src/actions/EditAndComposeAction",
"./src/actions/EditAction",
"./src/actions/PropertiesAction",
@@ -36,6 +37,7 @@ define([
"./src/policies/EditActionPolicy",
"./src/policies/EditableLinkPolicy",
"./src/policies/EditableMovePolicy",
"./src/policies/EditNavigationPolicy",
"./src/policies/EditContextualActionPolicy",
"./src/representers/EditRepresenter",
"./src/representers/EditToolbarRepresenter",
@@ -63,6 +65,7 @@ define([
EditPanesController,
ElementsController,
EditObjectController,
MCTBeforeUnload,
EditAndComposeAction,
EditAction,
PropertiesAction,
@@ -74,6 +77,7 @@ define([
EditActionPolicy,
EditableLinkPolicy,
EditableMovePolicy,
EditNavigationPolicy,
EditContextualActionPolicy,
EditRepresenter,
EditToolbarRepresenter,
@@ -128,7 +132,7 @@ define([
"depends": [
"$scope",
"$location",
"navigationService"
"policyService"
]
},
{
@@ -148,6 +152,15 @@ define([
]
}
],
"directives": [
{
"key": "mctBeforeUnload",
"implementation": MCTBeforeUnload,
"depends": [
"$window"
]
}
],
"actions": [
{
"key": "compose",
@@ -163,7 +176,7 @@ define([
],
"description": "Edit",
"category": "view-control",
"cssClass": "major icon-pencil"
"cssclass": "major icon-pencil"
},
{
"key": "properties",
@@ -172,7 +185,7 @@ define([
"view-control"
],
"implementation": PropertiesAction,
"cssClass": "major icon-pencil",
"cssclass": "major icon-pencil",
"name": "Edit Properties...",
"description": "Edit properties of this object.",
"depends": [
@@ -183,7 +196,7 @@ define([
"key": "remove",
"category": "contextual",
"implementation": RemoveAction,
"cssClass": "icon-trash",
"cssclass": "icon-trash",
"name": "Remove",
"description": "Remove this object from its containing object.",
"depends": [
@@ -195,7 +208,7 @@ define([
"category": "save",
"implementation": SaveAndStopEditingAction,
"name": "Save and Finish Editing",
"cssClass": "icon-save labeled",
"cssclass": "icon-save labeled",
"description": "Save changes made to these objects.",
"depends": [
"dialogService",
@@ -207,7 +220,7 @@ define([
"category": "save",
"implementation": SaveAction,
"name": "Save and Continue Editing",
"cssClass": "icon-save labeled",
"cssclass": "icon-save labeled",
"description": "Save changes made to these objects.",
"depends": [
"dialogService",
@@ -219,7 +232,7 @@ define([
"category": "save",
"implementation": SaveAsAction,
"name": "Save As...",
"cssClass": "icon-save labeled",
"cssclass": "icon-save labeled",
"description": "Save changes made to these objects.",
"depends": [
"$injector",
@@ -237,7 +250,7 @@ define([
// Because we use the name as label for edit buttons and mct-control buttons need
// the label to be set to undefined in order to not apply the labeled CSS rule.
"name": undefined,
"cssClass": "icon-x no-label",
"cssclass": "icon-x no-label",
"description": "Discard changes made to these objects.",
"depends": []
}
@@ -260,6 +273,11 @@ define([
"category": "action",
"implementation": EditableLinkPolicy
},
{
"category": "navigation",
"message": "Continuing will cause the loss of any unsaved changes.",
"implementation": EditNavigationPolicy
},
{
"implementation": CreationPolicy,
"category": "creation"
@@ -374,6 +392,7 @@ define([
{
"implementation": EditRepresenter,
"depends": [
"$q",
"$log"
]
},

View File

@@ -25,14 +25,14 @@
<li ng-repeat="createAction in createActions" ng-click="createAction.perform()">
<a ng-mouseover="representation.activeMetadata = createAction.getMetadata()"
ng-mouseleave="representation.activeMetadata = undefined"
class="menu-item-a {{ createAction.getMetadata().cssClass }}">
class="menu-item-a {{ createAction.getMetadata().cssclass }}">
{{createAction.getMetadata().name}}
</a>
</li>
</ul>
</div>
<div class="pane right menu-item-description">
<div class="desc-area icon {{ representation.activeMetadata.cssClass }}"></div>
<div class="desc-area icon {{ representation.activeMetadata.cssclass }}"></div>
<div class="desc-area title">
{{representation.activeMetadata.name}}
</div>

View File

@@ -26,7 +26,7 @@
structure="{
text: saveActions[0].getMetadata().name,
click: actionPerformer(saveActions[0]),
cssClass: 'major ' + saveActions[0].getMetadata().cssClass
cssclass: 'major ' + saveActions[0].getMetadata().cssclass
}">
</mct-control>
</span>
@@ -36,7 +36,7 @@
structure="{
options: saveActionsAsMenuOptions,
click: saveActionMenuClickHandler,
cssClass: 'btn-bar right icon-save no-label major'
cssclass: 'btn-bar right icon-save no-label major'
}">
</mct-control>
</span>
@@ -46,7 +46,7 @@
structure="{
text: currentAction.getMetadata().name,
click: actionPerformer(currentAction),
cssClass: currentAction.getMetadata().cssClass
cssclass: currentAction.getMetadata().cssclass
}">
</mct-control>
</span>

View File

@@ -20,7 +20,8 @@
at runtime from the About dialog for additional information.
-->
<div class="abs l-flex-col" ng-controller="EditObjectController as EditObjectController">
<div class="holder flex-elem l-flex-row object-browse-bar ">
<div mct-before-unload="EditObjectController.getUnloadWarning()"
class="holder flex-elem l-flex-row object-browse-bar ">
<div class="items-select left flex-elem l-flex-row grows">
<mct-representation key="'back-arrow'"
mct-object="domainObject"

View File

@@ -56,13 +56,13 @@ define(
//navigate back to parent because nothing to show.
return domainObject.getCapability("location").getOriginal().then(function (original) {
parent = original.getCapability("context").getParent();
return parent.getCapability("action").perform("navigate");
parent.getCapability("action").perform("navigate");
});
}
}
function cancel() {
return domainObject.getCapability("editor").finish();
function cancel(allowed) {
return allowed && domainObject.getCapability("editor").finish();
}
//Do navigation first in order to trigger unsaved changes dialog

View File

@@ -48,10 +48,9 @@ define(
* Decorate PersistenceCapability to queue persistence calls when a
* transaction is in progress.
*/
TransactionCapabilityDecorator.prototype.getCapabilities = function () {
TransactionCapabilityDecorator.prototype.getCapabilities = function (model) {
var self = this,
capabilities = this.capabilityService.getCapabilities
.apply(this.capabilityService, arguments),
capabilities = this.capabilityService.getCapabilities(model),
persistenceCapability = capabilities.persistence;
capabilities.persistence = function (domainObject) {

View File

@@ -41,7 +41,7 @@ define(
return {
key: action,
name: action.getMetadata().name,
cssClass: action.getMetadata().cssClass
cssclass: action.getMetadata().cssclass
};
}

View File

@@ -28,49 +28,18 @@ define(
[],
function () {
function isDirty(domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor");
return editorCapability &&
editorCapability.isEditContextRoot() &&
editorCapability.dirty();
}
function cancelEditing(domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor");
return editorCapability &&
editorCapability.finish();
}
/**
* Controller which is responsible for populating the scope for
* Edit mode
* @memberof platform/commonUI/edit
* @constructor
*/
function EditObjectController($scope, $location, navigationService) {
function EditObjectController($scope, $location, policyService) {
this.scope = $scope;
var domainObject = $scope.domainObject;
this.policyService = policyService;
var removeCheck = navigationService
.checkBeforeNavigation(function () {
if (isDirty(domainObject)) {
return "Continuing will cause the loss of any unsaved changes.";
}
return false;
});
$scope.$on('$destroy', function () {
removeCheck();
cancelEditing(domainObject);
});
function setViewForDomainObject() {
var navigatedObject;
function setViewForDomainObject(domainObject) {
var locationViewKey = $location.search().view;
@@ -85,15 +54,34 @@ define(
((domainObject && domainObject.useCapability('view')) || [])
.forEach(selectViewIfMatching);
}
navigatedObject = domainObject;
}
setViewForDomainObject();
$scope.$watch('domainObject', setViewForDomainObject);
$scope.doAction = function (action) {
return $scope[action] && $scope[action]();
};
}
/**
* Get the warning to show if the user attempts to navigate
* away from Edit mode while unsaved changes are present.
* @returns {string} the warning to show, or undefined if
* there are no unsaved changes
*/
EditObjectController.prototype.getUnloadWarning = function () {
var navigatedObject = this.scope.domainObject,
policyMessage;
this.policyService.allow("navigation", navigatedObject, undefined, function (message) {
policyMessage = message;
});
return policyMessage;
};
return EditObjectController;
}
);

View File

@@ -51,7 +51,7 @@ define(
function AddAction(type, parent, context, $q, dialogService, policyService) {
this.metadata = {
key: 'add',
cssClass: type.getCssClass(),
cssclass: type.getCssClass(),
name: type.getName(),
type: type.getKey(),
description: type.getDescription(),

View File

@@ -54,7 +54,8 @@ define(
AddActionProvider.prototype.getActions = function (actionContext) {
var context = actionContext || {},
key = context.key,
destination = context.domainObject;
destination = context.domainObject,
self = this;
// We only provide Add actions, and we need a
// domain object to serve as the container for the
@@ -65,16 +66,18 @@ define(
}
// Introduce one create action per type
return ['timeline', 'activity'].map(function (type) {
return this.typeService.listTypes().filter(function (type) {
return self.policyService.allow("creation", type) && self.policyService.allow("composition", destination.getCapability('type'), type);
}).map(function (type) {
return new AddAction(
this.typeService.getType(type),
type,
destination,
context,
this.$q,
this.dialogService,
this.policyService
self.$q,
self.dialogService,
self.policyService
);
}, this);
});
};
return AddActionProvider;

View File

@@ -47,7 +47,7 @@ define(
function CreateAction(type, parent, context) {
this.metadata = {
key: 'create',
cssClass: type.getCssClass(),
cssclass: type.getCssClass(),
name: type.getName(),
type: type.getKey(),
description: type.getDescription(),

View File

@@ -56,16 +56,16 @@ define(
*/
CreateWizard.prototype.getFormStructure = function (includeLocation) {
var sections = [],
domainObject = this.domainObject,
type = this.type,
policyService = this.policyService;
function validateLocation(parent) {
var parentType = parent &&
parent.getCapability('type');
return parentType && policyService.allow(
function validateLocation(locatingObject) {
var locatingType = locatingObject &&
locatingObject.getCapability('type');
return locatingType && policyService.allow(
"composition",
parentType,
domainObject
locatingType,
type
);
}
@@ -91,7 +91,7 @@ define(
if (includeLocation) {
sections.push({
name: 'Location',
cssClass: "grows",
cssclass: "grows",
rows: [{
name: "Save In",
control: "locator",

View File

@@ -0,0 +1,104 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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 () {
/**
* Defines the `mct-before-unload` directive. The expression bound
* to this attribute will be evaluated during page navigation events
* and, if it returns a truthy value, will be used to populate a
* prompt to the user to confirm this navigation.
* @memberof platform/commonUI/edit
* @constructor
* @param $window the window
*/
function MCTBeforeUnload($window) {
var unloads = [],
oldBeforeUnload = $window.onbeforeunload;
// Run all unload functions, returning the first returns truthily.
function checkUnloads() {
var result;
unloads.forEach(function (unload) {
result = result || unload();
});
return result;
}
// Link function for an mct-before-unload directive usage
function link(scope, element, attrs) {
// Invoke the
function unload() {
return scope.$eval(attrs.mctBeforeUnload);
}
// Stop using this unload expression
function removeUnload() {
unloads = unloads.filter(function (callback) {
return callback !== unload;
});
if (unloads.length === 0) {
$window.onbeforeunload = oldBeforeUnload;
}
}
// Show a dialog before allowing a location change
function checkLocationChange(event) {
// Get an unload message (if any)
var warning = unload();
// Prompt the user if there's an unload message
if (warning && !$window.confirm(warning)) {
// ...and prevent the route change if it was confirmed
event.preventDefault();
}
}
// If this is the first active instance of this directive,
// register as the window's beforeunload handler
if (unloads.length === 0) {
$window.onbeforeunload = checkUnloads;
}
// Include this instance of the directive's unload function
unloads.push(unload);
// Remove it when the scope is destroyed
scope.$on("$destroy", removeUnload);
// Also handle route changes
scope.$on("$locationChangeStart", checkLocationChange);
}
return {
// Applicable as an attribute
restrict: "A",
// Link with the provided function
link: link
};
}
return MCTBeforeUnload;
}
);

View File

@@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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 () {
/**
* Policy controlling whether navigation events should proceed
* when object is being edited.
* @memberof platform/commonUI/edit
* @constructor
* @implements {Policy.<Action, ActionContext>}
*/
function EditNavigationPolicy(policyService) {
this.policyService = policyService;
}
/**
* @private
*/
EditNavigationPolicy.prototype.isDirty = function (domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor");
return editorCapability &&
editorCapability.isEditContextRoot() &&
editorCapability.dirty();
};
/**
* Allow navigation if an object is not dirty, or if the user elects
* to proceed anyway.
* @param currentNavigation
* @returns {boolean|*} true if the object model is clean; or if
* it's dirty and the user wishes to proceed anyway.
*/
EditNavigationPolicy.prototype.allow = function (currentNavigation) {
return !this.isDirty(currentNavigation);
};
return EditNavigationPolicy;
}
);

View File

@@ -43,55 +43,98 @@ define(
* @implements {Representer}
* @constructor
*/
function EditRepresenter($log, $scope) {
this.$log = $log;
this.$scope = $scope;
function EditRepresenter($q, $log, scope) {
var self = this;
this.scope = scope;
this.listenHandle = undefined;
// Mutate and persist a new version of a domain object's model.
function doMutate(model) {
var domainObject = self.domainObject;
// First, mutate; then, persist.
return $q.when(domainObject.useCapability("mutation", function () {
return model;
}));
}
// Handle changes to model and/or view configuration
function commit(message) {
// Look up from scope; these will have been populated by
// mct-representation.
var model = scope.model,
configuration = scope.configuration,
domainObject = self.domainObject;
// Log the commit message
$log.debug([
"Committing ",
domainObject && domainObject.getModel().name,
"(" + (domainObject && domainObject.getId()) + "):",
message
].join(" "));
// Update the configuration stored in the model, and persist.
if (domainObject) {
// Configurations for specific views are stored by
// key in the "configuration" field of the model.
if (self.key && configuration) {
model.configuration = model.configuration || {};
model.configuration[self.key] = configuration;
}
doMutate(model);
}
}
// Place the "commit" method in the scope
scope.commit = commit;
// Clean up when the scope is destroyed
scope.$on("$destroy", function () {
self.destroy();
});
this.$scope.commit = this.commit.bind(this);
}
/**
* Commit any changes made to the in-scope model to the domain object.
* Also commits any changes made to $scope.configuration to the proper
* configuration value for the current representation.
*
* @param {String} message a message to log with the commit message.
*/
EditRepresenter.prototype.commit = function (message) {
var model = this.$scope.model,
configuration = this.$scope.configuration,
domainObject = this.domainObject;
this.$log.debug([
"Committing ",
domainObject && domainObject.getModel().name,
"(" + (domainObject && domainObject.getId()) + "):",
message
].join(" "));
if (this.domainObject) {
if (this.key && configuration) {
model.configuration = model.configuration || {};
model.configuration[this.key] = configuration;
}
domainObject.useCapability('mutation', function () {
return model;
});
}
};
// Handle a specific representation of a specific domain object
EditRepresenter.prototype.represent = function (representation, representedObject) {
EditRepresenter.prototype.represent = function represent(representation, representedObject) {
var scope = this.scope;
// Track the key, to know which view configuration to save to.
this.key = (representation || {}).key;
// Track the represented object
this.domainObject = representedObject;
if (representation) {
this.key = representation.key;
} else {
delete this.key;
// Ensure existing watches are released
this.destroy();
function setEditing() {
scope.viewObjectTemplate = 'edit-object';
}
/**
* Listen for changes in object state. If the object becomes
* editable then change the view and inspector regions
* object representation accordingly
*/
this.listenHandle = this.domainObject.getCapability('status').listen(function (statuses) {
if (statuses.indexOf('editing') !== -1) {
setEditing();
} else {
delete scope.viewObjectTemplate;
}
});
if (representedObject.hasCapability('editor') && representedObject.getCapability('editor').isEditContextRoot()) {
setEditing();
}
};
// Respond to the destruction of the current representation.
EditRepresenter.prototype.destroy = function () {};
EditRepresenter.prototype.destroy = function destroy() {
return this.listenHandle && this.listenHandle();
};
return EditRepresenter;
}

View File

@@ -27,7 +27,6 @@ define(
describe("The Save As action", function () {
var mockDomainObject,
mockClonedObject,
mockEditorCapability,
mockActionCapability,
mockObjectService,
@@ -68,24 +67,18 @@ define(
return capabilities[capability];
});
mockDomainObject.getModel.andReturn({location: 'a', persisted: undefined});
mockDomainObject.getId.andReturn(0);
mockClonedObject = jasmine.createSpyObj(
"clonedObject",
[
"getId"
]
);
mockClonedObject.getId.andReturn(1);
mockDomainObject.getId.andReturn("mockObject");
mockParent = jasmine.createSpyObj(
"parentObject",
[
"getCapability",
"hasCapability",
"getModel"
"getModel",
"getId"
]
);
mockParent.getId.andReturn("parentId");
mockEditorCapability = jasmine.createSpyObj(
"editor",
@@ -123,7 +116,10 @@ define(
"perform"
]
);
mockCopyService.perform.andReturn(mockPromise(mockClonedObject));
mockCopyService.perform.andCallFake(function (object) {
return mockPromise(object);
});
mockNotificationService = jasmine.createSpyObj(
"notificationService",

View File

@@ -28,7 +28,7 @@ define(
describe("The Edit Action controller", function () {
var mockSaveActionMetadata = {
name: "mocked-save-action",
cssClass: "mocked-save-action-css"
cssclass: "mocked-save-action-css"
};
function fakeGetActions(actionContext) {
@@ -86,7 +86,7 @@ define(
expect(menuOptions[1].key).toEqual(mockScope.saveActions[1]);
menuOptions.forEach(function (option) {
expect(option.name).toEqual(mockSaveActionMetadata.name);
expect(option.cssClass).toEqual(mockSaveActionMetadata.cssClass);
expect(option.cssclass).toEqual(mockSaveActionMetadata.cssclass);
});
});

View File

@@ -24,19 +24,32 @@ define(
["../../src/controllers/EditObjectController"],
function (EditObjectController) {
describe("The Edit Object controller", function () {
describe("The Edit mode controller", function () {
var mockScope,
mockObject,
testViews,
mockEditorCapability,
mockType,
mockLocation,
mockNavigationService,
removeCheck,
mockStatusCapability,
mockCapabilities,
mockPolicyService,
controller;
// Utility function; look for a $watch on scope and fire it
function fireWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockPolicyService = jasmine.createSpyObj(
"policyService",
[
"allow"
]
);
mockScope = jasmine.createSpyObj(
"$scope",
["$on", "$watch"]
@@ -45,16 +58,16 @@ define(
"domainObject",
["getId", "getModel", "getCapability", "hasCapability", "useCapability"]
);
mockEditorCapability = jasmine.createSpyObj(
"mockEditorCapability",
["isEditContextRoot", "dirty", "finish"]
mockType = jasmine.createSpyObj(
"type",
["hasFeature"]
);
mockStatusCapability = jasmine.createSpyObj('statusCapability',
["get"]
);
mockCapabilities = {
"editor" : mockEditorCapability,
"type" : mockType,
"status": mockStatusCapability
};
@@ -62,70 +75,52 @@ define(
["search"]
);
mockLocation.search.andReturn({"view": "fixed"});
mockNavigationService = jasmine.createSpyObj('navigationService',
["checkBeforeNavigation"]
);
removeCheck = jasmine.createSpy('removeCheck');
mockNavigationService.checkBeforeNavigation.andReturn(removeCheck);
mockObject.getId.andReturn("test");
mockObject.getModel.andReturn({ name: "Test object" });
mockObject.getCapability.andCallFake(function (key) {
return mockCapabilities[key];
});
testViews = [
{ key: 'abc' },
{ key: 'def', someKey: 'some value' },
{ key: 'xyz' }
];
mockObject.useCapability.andCallFake(function (c) {
return (c === 'view') && testViews;
});
mockLocation.search.andReturn({ view: 'def' });
mockType.hasFeature.andReturn(true);
mockScope.domainObject = mockObject;
controller = new EditObjectController(
mockScope,
mockLocation,
mockNavigationService
mockPolicyService
);
});
it("adds a check before navigation", function () {
expect(mockNavigationService.checkBeforeNavigation)
.toHaveBeenCalledWith(jasmine.any(Function));
it("exposes a warning message for unload", function () {
var errorMessage = "Unsaved changes";
var checkFn = mockNavigationService.checkBeforeNavigation.mostRecentCall.args[0];
// Normally, should be undefined
expect(controller.getUnloadWarning()).toBeUndefined();
mockEditorCapability.isEditContextRoot.andReturn(false);
mockEditorCapability.dirty.andReturn(false);
expect(checkFn()).toBe(false);
mockEditorCapability.isEditContextRoot.andReturn(true);
expect(checkFn()).toBe(false);
mockEditorCapability.dirty.andReturn(true);
expect(checkFn())
.toBe("Continuing will cause the loss of any unsaved changes.");
// Override the policy service to prevent navigation
mockPolicyService.allow.andCallFake(function (category, object, context, callback) {
callback(errorMessage);
});
// Should have some warning message here now
expect(controller.getUnloadWarning()).toEqual(errorMessage);
});
it("cleans up on destroy", function () {
expect(mockScope.$on)
.toHaveBeenCalledWith("$destroy", jasmine.any(Function));
mockScope.$on.mostRecentCall.args[1]();
expect(mockEditorCapability.finish).toHaveBeenCalled();
expect(removeCheck).toHaveBeenCalled();
});
it("sets the active view from query parameters", function () {
var testViews = [
{ key: 'abc' },
{ key: 'def', someKey: 'some value' },
{ key: 'xyz' }
];
mockObject.useCapability.andCallFake(function (c) {
return (c === 'view') && testViews;
});
mockLocation.search.andReturn({ view: 'def' });
fireWatch('domainObject', mockObject);
expect(mockScope.representation.selected)
.toEqual(testViews[1]);
});

View File

@@ -31,7 +31,9 @@ define(
var mockTypeService,
mockDialogService,
mockPolicyService,
mockTypeMap,
mockCreationPolicy,
mockCompositionPolicy,
mockPolicyMap = {},
mockTypes,
mockDomainObject,
mockQ,
@@ -53,33 +55,49 @@ define(
);
mockType.hasFeature.andReturn(true);
mockType.getName.andReturn(name);
mockType.getKey.andReturn(name);
return mockType;
}
beforeEach(function () {
mockTypeService = jasmine.createSpyObj(
"typeService",
["getType"]
["listTypes"]
);
mockDialogService = jasmine.createSpyObj(
"dialogService",
["getUserInput"]
);
mockPolicyService = jasmine.createSpyObj(
"policyService",
["allow"]
);
mockDialogService = {};
mockPolicyService = {};
mockDomainObject = {};
mockTypes = [
"timeline",
"activity",
"other"
].map(createMockType);
mockTypeMap = {};
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getCapability"]
);
//Mocking getCapability because AddActionProvider uses the
// type capability of the destination object.
mockDomainObject.getCapability.andReturn({});
mockTypes = ["A", "B", "C"].map(createMockType);
mockTypes.forEach(function (type) {
mockTypeMap[type.getKey()] = type;
mockPolicyMap[type.getName()] = true;
});
mockTypeService.getType.andCallFake(function (key) {
return mockTypeMap[key];
});
mockCreationPolicy = function (type) {
return mockPolicyMap[type.getName()];
};
mockCompositionPolicy = function () {
return true;
};
mockPolicyService.allow.andReturn(true);
mockTypeService.listTypes.andReturn(mockTypes);
provider = new AddActionProvider(
mockQ,
@@ -89,16 +107,29 @@ define(
);
});
it("provides actions for timeline and activity", function () {
var actions = provider.getActions({
it("checks for creatability", function () {
provider.getActions({
key: "add",
domainObject: mockDomainObject
});
expect(actions.length).toBe(2);
expect(actions[0].metadata.type).toBe('timeline');
expect(actions[1].metadata.type).toBe('activity');
// Make sure it was creation which was used to check
expect(mockPolicyService.allow)
.toHaveBeenCalledWith("creation", mockTypes[0]);
});
it("checks for composability of type", function () {
provider.getActions({
key: "add",
domainObject: mockDomainObject
});
expect(mockPolicyService.allow).toHaveBeenCalledWith(
"composition",
jasmine.any(Object),
jasmine.any(Object)
);
expect(mockDomainObject.getCapability).toHaveBeenCalledWith('type');
});
});
}

View File

@@ -138,7 +138,7 @@ define(
expect(metadata.name).toEqual("Test");
expect(metadata.description).toEqual("a test type");
expect(metadata.cssClass).toEqual("icon-telemetry");
expect(metadata.cssclass).toEqual("icon-telemetry");
});
describe("the perform function", function () {

View File

@@ -175,7 +175,7 @@ define(
expect(mockPolicyService.allow).toHaveBeenCalledWith(
'composition',
mockOtherType,
mockDomainObject
mockType
);
});

View File

@@ -0,0 +1,114 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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/directives/MCTBeforeUnload"],
function (MCTBeforeUnload) {
describe("The mct-before-unload directive", function () {
var mockWindow,
mockScope,
testAttrs,
mockEvent,
directive;
function fireListener(eventType, value) {
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === eventType) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockWindow = jasmine.createSpyObj("$window", ['confirm']);
mockScope = jasmine.createSpyObj("$scope", ['$eval', '$on']);
testAttrs = { mctBeforeUnload: "someExpression" };
mockEvent = jasmine.createSpyObj("event", ["preventDefault"]);
directive = new MCTBeforeUnload(mockWindow);
directive.link(mockScope, {}, testAttrs);
});
it("can be used only as an attribute", function () {
expect(directive.restrict).toEqual('A');
});
it("listens for beforeunload", function () {
expect(mockWindow.onbeforeunload).toEqual(jasmine.any(Function));
});
it("listens for route changes", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
"$locationChangeStart",
jasmine.any(Function)
);
});
it("listens for its scope's destroy event", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
"$destroy",
jasmine.any(Function)
);
});
it("uses result of evaluated expression as a warning", function () {
mockScope.$eval.andReturn(undefined);
expect(mockWindow.onbeforeunload(mockEvent)).toBeUndefined();
mockScope.$eval.andReturn("some message");
expect(mockWindow.onbeforeunload(mockEvent)).toEqual("some message");
// Verify that the right expression was evaluated
expect(mockScope.$eval).toHaveBeenCalledWith(testAttrs.mctBeforeUnload);
});
it("confirms route changes", function () {
// First, try with no unsaved changes;
// should not confirm or preventDefault
mockScope.$eval.andReturn(undefined);
fireListener("$locationChangeStart", mockEvent);
expect(mockWindow.confirm).not.toHaveBeenCalled();
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
// Next, try with unsaved changes that the user confirms;
// should prompt, but not preventDefault
mockScope.$eval.andReturn("some message");
mockWindow.confirm.andReturn(true);
fireListener("$locationChangeStart", mockEvent);
expect(mockWindow.confirm).toHaveBeenCalledWith("some message");
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
// Finally, act as if the user said no to this dialog;
// this should preventDefault on the location change.
mockWindow.confirm.andReturn(false);
fireListener("$locationChangeStart", mockEvent);
expect(mockWindow.confirm).toHaveBeenCalledWith("some message");
expect(mockEvent.preventDefault).toHaveBeenCalled();
});
it("cleans up listeners when destroyed", function () {
fireListener("$destroy", mockEvent);
expect(mockWindow.onbeforeunload).toBeUndefined();
});
});
}
);

View File

@@ -20,70 +20,103 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../../src/representers/EditRepresenter'
], function (
EditRepresenter
) {
describe('EditRepresenter', function () {
var $log,
$scope,
representer;
define(
["../../src/representers/EditRepresenter"],
function (EditRepresenter) {
describe("The Edit mode representer", function () {
var mockQ,
mockLog,
mockScope,
testRepresentation,
mockDomainObject,
mockStatusCapability,
mockEditorCapability,
mockCapabilities,
representer;
beforeEach(function () {
$log = jasmine.createSpyObj('$log', ['debug']);
$scope = {};
representer = new EditRepresenter($log, $scope);
});
it('injects a commit function in scope', function () {
expect($scope.commit).toEqual(jasmine.any(Function));
});
describe('representation', function () {
var domainObject,
representation;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
domainObject = jasmine.createSpyObj('domainObject', [
'getId',
'getModel',
'useCapability'
mockQ = { when: mockPromise };
mockLog = jasmine.createSpyObj("$log", ["info", "debug"]);
mockScope = jasmine.createSpyObj("$scope", ["$watch", "$on"]);
testRepresentation = { key: "test" };
mockDomainObject = jasmine.createSpyObj("domainObject", [
"getId",
"getModel",
"getCapability",
"useCapability",
"hasCapability"
]);
mockStatusCapability =
jasmine.createSpyObj("statusCapability", ["listen"]);
mockEditorCapability =
jasmine.createSpyObj("editorCapability", ["isEditContextRoot"]);
domainObject.getId.andReturn('anId');
domainObject.getModel.andReturn({name: 'anObject'});
representation = {
key: 'someRepresentation'
mockCapabilities = {
'status': mockStatusCapability,
'editor': mockEditorCapability
};
$scope.model = {name: 'anotherName'};
$scope.configuration = {some: 'config'};
representer.represent(representation, domainObject);
mockDomainObject.getModel.andReturn({});
mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.useCapability.andReturn(true);
mockDomainObject.getCapability.andCallFake(function (capability) {
return mockCapabilities[capability];
});
representer = new EditRepresenter(mockQ, mockLog, mockScope);
representer.represent(testRepresentation, mockDomainObject);
});
it('logs a message when commiting', function () {
$scope.commit('Test Message');
expect($log.debug)
.toHaveBeenCalledWith('Committing anObject (anId): Test Message');
it("provides a commit method in scope", function () {
expect(mockScope.commit).toEqual(jasmine.any(Function));
});
it('mutates the object when committing', function () {
$scope.commit('Test Message');
expect(domainObject.useCapability)
.toHaveBeenCalledWith('mutation', jasmine.any(Function));
var mutateValue = domainObject.useCapability.calls[0].args[1]();
expect(mutateValue.configuration.someRepresentation)
.toEqual({some: 'config'});
expect(mutateValue.name).toEqual('anotherName');
it("Sets edit view template on edit mode", function () {
mockStatusCapability.listen.mostRecentCall.args[0](['editing']);
mockEditorCapability.isEditContextRoot.andReturn(true);
expect(mockScope.viewObjectTemplate).toEqual('edit-object');
});
it("Cleans up listeners on scope destroy", function () {
representer.listenHandle = jasmine.createSpy('listen');
mockScope.$on.mostRecentCall.args[1]();
expect(representer.listenHandle).toHaveBeenCalled();
});
it("mutates upon observed changes", function () {
mockScope.model = { someKey: "some value" };
mockScope.configuration = { someConfiguration: "something" };
mockScope.commit("Some message");
// Should have mutated the object...
expect(mockDomainObject.useCapability).toHaveBeenCalledWith(
"mutation",
jasmine.any(Function)
);
// Finally, check that the provided mutation function
// includes both model and configuration
expect(
mockDomainObject.useCapability.mostRecentCall.args[1]()
).toEqual({
someKey: "some value",
configuration: {
test: { someConfiguration: "something" }
}
});
});
});
});
});
}
);

View File

@@ -55,7 +55,9 @@
height: 70%;
width: 50%;
min-height: 300px;
max-height: 800px;
min-width: 600px;
max-width: 1000px;
z-index: 101;
> .contents {
$m: $overlayMargin;
@@ -139,4 +141,4 @@
$h: 225px;
min-height: $h;
height: $h;
}
}

View File

@@ -19,7 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
$output-bourbon-deprecation-warnings: false;
@import "bourbon";
@import "logo-and-bg";

View File

@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<a class="s-button key-{{parameters.action.getMetadata().key}} {{parameters.action.getMetadata().cssClass}}"
<a class="s-button key-{{parameters.action.getMetadata().key}} {{parameters.action.getMetadata().cssclass}}"
ng-class="{ labeled: parameters.labeled }"
title="{{parameters.action.getMetadata().description}}"
ng-click="parameters.action.perform()">

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<span ng-controller="ViewSwitcherController">
<div class="view-switcher menu-element s-menu-button {{ngModel.selected.cssClass}}"
<div class="view-switcher menu-element s-menu-button {{ngModel.selected.cssclass}}"
ng-if="view.length > 1"
ng-controller="ClickAwayController as toggle">
@@ -33,7 +33,7 @@
<ul>
<li ng-repeat="option in view"
ng-click="ngModel.selected = option; toggle.setState(false)"
class="{{option.cssClass}}">
class="{{option.cssclass}}">
{{option.name}}
</li>
</ul>

View File

@@ -25,7 +25,7 @@
<li ng-repeat="menuAction in menuActions"
ng-click="menuAction.perform()"
title="{{menuAction.getMetadata().description}}"
class="{{menuAction.getMetadata().cssClass}}">
class="{{menuAction.getMetadata().cssclass}}">
{{menuAction.getMetadata().name}}
</li>
</ul>

View File

@@ -19,9 +19,6 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<mct-tree root-object="domainObject"
selected-object="ngModel.selectedObject"
on-selection="ngModel.onSelection"
allow-selection="ngModel.allowSelection">
<mct-tree mct-object="domainObject" mct-model="ngModel.selectedObject">
</mct-tree>

View File

@@ -26,54 +26,25 @@ define([
], function (angular, TreeView) {
function MCTTree(gestureService) {
function link(scope, element) {
if (!scope.allowSelection) {
scope.allowSelection = function () {
return true;
};
}
if (!scope.onSelection) {
scope.onSelection = function () {};
}
var currentSelection = scope.selectedObject;
var treeView = new TreeView(gestureService);
function setSelection(domainObject, event) {
if (currentSelection === domainObject) {
return;
}
if (!scope.allowSelection(domainObject)) {
treeView.value(currentSelection);
return;
}
currentSelection = domainObject;
scope.onSelection(domainObject);
scope.selectedObject = domainObject;
if (event && event instanceof MouseEvent) {
scope.$apply();
}
}
var unobserve = treeView.observe(setSelection);
var treeView = new TreeView(gestureService),
unobserve = treeView.observe(function (domainObject) {
if (scope.mctModel !== domainObject) {
scope.mctModel = domainObject;
scope.$apply();
}
});
element.append(angular.element(treeView.elements()));
scope.$watch('selectedObject', function (object) {
currentSelection = object;
treeView.value(object);
});
scope.$watch('rootObject', treeView.model.bind(treeView));
scope.$watch('mctModel', treeView.value.bind(treeView));
scope.$watch('mctObject', treeView.model.bind(treeView));
scope.$on('$destroy', unobserve);
}
return {
restrict: "E",
link: link,
scope: {
rootObject: "=",
selectedObject: "=",
onSelection: "=?",
allowSelection: "=?"
}
scope: { mctObject: "=", mctModel: "=" }
};
}

View File

@@ -49,8 +49,8 @@ define([
this.labelView = new TreeLabelView(gestureService);
$(this.labelView.elements()).on('click', function (event) {
selectFn(this.activeObject, event);
$(this.labelView.elements()).on('click', function () {
selectFn(this.activeObject);
}.bind(this));
this.li.append($(nodeTemplate));

View File

@@ -109,11 +109,11 @@ define([
}.bind(this));
};
TreeView.prototype.value = function (domainObject, event) {
TreeView.prototype.value = function (domainObject) {
this.selectedObject = domainObject;
this.updateNodeViewSelection();
this.callbacks.forEach(function (callback) {
callback(domainObject, event);
callback(domainObject);
});
};

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.
*****************************************************************************/
/* global console*/
define([
'../../src/directives/MCTTree',
'../../src/ui/TreeView'
], function (MCTTree, TreeView) {
'../../src/directives/MCTTree'
], function (MCTTree) {
describe("The mct-tree directive", function () {
var mockParse,
mockGestureService,
@@ -52,7 +50,6 @@ define([
mockExpr = jasmine.createSpy('expr');
mockExpr.assign = jasmine.createSpy('assign');
mockParse.andReturn(mockExpr);
spyOn(TreeView.prototype, 'observe').andCallThrough();
mctTree = new MCTTree(mockParse, mockGestureService);
});
@@ -61,13 +58,8 @@ define([
expect(mctTree.restrict).toEqual("E");
});
it("two-way binds", function () {
expect(mctTree.scope).toEqual({
rootObject: "=",
selectedObject: "=",
allowSelection: "=?",
onSelection: "=?"
});
it("two-way binds to mctObject and mctModel", function () {
expect(mctTree.scope).toEqual({ mctObject: "=", mctModel: "=" });
});
describe("link", function () {
@@ -89,16 +81,16 @@ define([
expect(mockElement.append).toHaveBeenCalled();
});
it("watches for selected-object expression in the parent", function () {
it("watches for mct-model's expression in the parent", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"selectedObject",
"mctModel",
jasmine.any(Function)
);
});
it("watches for changes to root-object", function () {
it("watches for changes to mct-object", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"rootObject",
"mctObject",
jasmine.any(Function)
);
});
@@ -110,10 +102,6 @@ define([
);
});
it("watches for changes in tree view", function () {
});
// https://github.com/nasa/openmct/issues/1114
it("does not trigger $apply during $watches", function () {
mockScope.mctObject = makeMockDomainObject('root');
@@ -123,18 +111,14 @@ define([
});
expect(mockScope.$apply).not.toHaveBeenCalled();
});
it("does trigger $apply from tree manipulation", function () {
if (/PhantomJS/g.test(window.navigator.userAgent)) {
console.log('Unable to run test in PhantomJS due to lack of support for event constructors');
return;
}
it("does trigger $apply from other value changes", function () {
// White-boxy; we know this is the setter for the tree's value
var treeValueFn = TreeView.prototype.observe.calls[0].args[0];
var treeValueFn = mockScope.$watch.calls[0].args[1];
mockScope.mctObject = makeMockDomainObject('root');
mockScope.mctMode = makeMockDomainObject('selection');
treeValueFn(makeMockDomainObject('other'), new MouseEvent("click"));
treeValueFn(makeMockDomainObject('other'));
expect(mockScope.$apply).toHaveBeenCalled();
});

View File

@@ -289,9 +289,8 @@ define([
});
it("notifies listeners when value is changed", function () {
treeView.value(mockDomainObject, {some: event});
expect(mockCallback)
.toHaveBeenCalledWith(mockDomainObject, {some: event});
treeView.value(mockDomainObject);
expect(mockCallback).toHaveBeenCalledWith(mockDomainObject);
});
it("does not notify listeners when deactivated", function () {

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
$output-bourbon-deprecation-warnings: false;
@import "bourbon";
@import "../../../../general/res/sass/_mixins";

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
$output-bourbon-deprecation-warnings: false;
@import "bourbon";
@import "../../../../general/res/sass/_mixins";

View File

@@ -40,6 +40,9 @@ define([
{
"category": "composition",
"implementation": CompositionPolicy,
"depends": [
"$injector"
],
"message": "Objects of this type cannot contain objects of that type."
},
{

View File

@@ -0,0 +1,77 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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 () {
/**
* Build a table indicating which types are expected to expose
* which capabilities. This supports composition policy (rules
* for which objects can contain which other objects) which
* sometimes is determined based on the presence of capabilities.
* @constructor
* @memberof platform/containment
*/
function CapabilityTable(typeService, capabilityService) {
var self = this;
// Build an initial model for a type
function buildModel(type) {
var model = Object.create(type.getInitialModel() || {});
model.type = type.getKey();
return model;
}
// Get capabilities expected for this type
function getCapabilities(type) {
return capabilityService.getCapabilities(buildModel(type));
}
// Populate the lookup table for this type's capabilities
function addToTable(type) {
var typeKey = type.getKey();
Object.keys(getCapabilities(type)).forEach(function (key) {
self.table[key] = self.table[key] || {};
self.table[key][typeKey] = true;
});
}
// Build the table
this.table = {};
(typeService.listTypes() || []).forEach(addToTable);
}
/**
* Check if a type is expected to expose a specific capability.
* @param {string} typeKey the type identifier
* @param {string} capabilityKey the capability identifier
* @returns {boolean} true if expected to be exposed
*/
CapabilityTable.prototype.hasCapability = function (typeKey, capabilityKey) {
return (this.table[capabilityKey] || {})[typeKey];
};
return CapabilityTable;
}
);

View File

@@ -45,7 +45,9 @@ define(
ComposeActionPolicy.prototype.allowComposition = function (containerObject, selectedObject) {
// Get the object types involved in the compose action
var containerType = containerObject &&
containerObject.getCapability('type');
containerObject.getCapability('type'),
selectedType = selectedObject &&
selectedObject.getCapability('type');
// Get a reference to the policy service if needed...
this.policyService = this.policyService || this.getPolicyService();
@@ -55,7 +57,7 @@ define(
this.policyService.allow(
'composition',
containerType,
selectedObject
selectedType
);
};

View File

@@ -26,8 +26,8 @@
* @namespace platform/containment
*/
define(
[],
function () {
['./ContainmentTable'],
function (ContainmentTable) {
/**
* Defines composition policy as driven by type metadata.
@@ -35,33 +35,21 @@ define(
* @memberof platform/containment
* @implements {Policy.<Type, Type>}
*/
function CompositionPolicy() {
function CompositionPolicy($injector) {
// We're really just wrapping the containment table and rephrasing
// it as a policy decision.
var table;
this.getTable = function () {
return (table = table || new ContainmentTable(
$injector.get('typeService'),
$injector.get('capabilityService')
));
};
}
CompositionPolicy.prototype.allow = function (parentType, child) {
var parentDef = parentType.getDefinition();
// A parent without containment rules can contain anything.
if (!parentDef.contains) {
return true;
}
// If any containment rule matches context type, the candidate
// can contain this type.
return parentDef.contains.some(function (c) {
// Simple containment rules are supported typeKeys.
if (typeof c === 'string') {
return c === child.getCapability('type').getKey();
}
// More complicated rules require context to have all specified
// capabilities.
if (!Array.isArray(c.has)) {
c.has = [c.has];
}
return c.has.every(function (capability) {
return child.hasCapability(capability);
});
});
CompositionPolicy.prototype.allow = function (candidate, context) {
return this.getTable().canContain(candidate, context);
};
return CompositionPolicy;

View File

@@ -0,0 +1,116 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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(
['./CapabilityTable'],
function (CapabilityTable) {
// Symbolic value for the type table for cases when any type
// is allowed to be contained.
var ANY = true;
/**
* Supports composition policy by maintaining a table of
* domain object types, to determine if they can contain
* other domain object types. This is determined at application
* start time (plug-in support means this cannot be determined
* prior to that, but we don't want to redo these calculations
* every time policy is checked.)
* @constructor
* @memberof platform/containment
*/
function ContainmentTable(typeService, capabilityService) {
var self = this,
types = typeService.listTypes(),
capabilityTable = new CapabilityTable(typeService, capabilityService);
// Add types which have all these capabilities to the set
// of allowed types
function addToSetByCapability(set, has) {
has = Array.isArray(has) ? has : [has];
types.forEach(function (type) {
var typeKey = type.getKey();
set[typeKey] = has.map(function (capabilityKey) {
return capabilityTable.hasCapability(typeKey, capabilityKey);
}).reduce(function (a, b) {
return a && b;
}, true);
});
}
// Add this type (or type description) to the set of allowed types
function addToSet(set, type) {
// Is this a simple case of an explicit type identifier?
if (typeof type === 'string') {
// If so, add it to the set of allowed types
set[type] = true;
} else {
// Otherwise, populate that set based on capabilities
addToSetByCapability(set, (type || {}).has || []);
}
}
// Add to the lookup table for this type
function addToTable(type) {
var key = type.getKey(),
definition = type.getDefinition() || {},
contains = definition.contains;
// Check for defined containment restrictions
if (contains === undefined) {
// If not, accept anything
self.table[key] = ANY;
} else {
// Start with an empty set...
self.table[key] = {};
// ...cast accepted types to array if necessary...
contains = Array.isArray(contains) ? contains : [contains];
// ...and add all containment rules to that set
contains.forEach(function (c) {
addToSet(self.table[key], c);
});
}
}
// Build the table
this.table = {};
types.forEach(addToTable);
}
/**
* Check if domain objects of one type can contain domain
* objects of another type.
* @param {Type} containerType type of the containing domain object
* @param {Type} containedType type of the domain object
* to be contained
* @returns {boolean} true if allowable
*/
ContainmentTable.prototype.canContain = function (containerType, containedType) {
var set = this.table[containerType.getKey()] || {};
// Recognize either the symbolic value for "can contain
// anything", or lookup the specific type from the set.
return (set === ANY) || set[containedType.getKey()];
};
return ContainmentTable;
}
);

View File

@@ -0,0 +1,85 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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/CapabilityTable"],
function (CapabilityTable) {
describe("Composition policy's capability table", function () {
var mockTypeService,
mockCapabilityService,
mockTypes,
table;
beforeEach(function () {
mockTypeService = jasmine.createSpyObj(
'typeService',
['listTypes']
);
mockCapabilityService = jasmine.createSpyObj(
'capabilityService',
['getCapabilities']
);
// Both types can only contain b, let's say
mockTypes = ['a', 'b'].map(function (type) {
var mockType = jasmine.createSpyObj(
'type-' + type,
['getKey', 'getDefinition', 'getInitialModel']
);
mockType.getKey.andReturn(type);
// Return a model to drive apparent capabilities
mockType.getInitialModel.andReturn({ id: type });
return mockType;
});
mockTypeService.listTypes.andReturn(mockTypes);
mockCapabilityService.getCapabilities.andCallFake(function (model) {
var capabilities = {};
capabilities[model.id + '-capability'] = true;
return capabilities;
});
table = new CapabilityTable(
mockTypeService,
mockCapabilityService
);
});
it("provides for lookup of capabilities by type", function () {
// Based on initial model, should report the presence
// of particular capabilities - suffixed above with -capability
expect(table.hasCapability('a', 'a-capability'))
.toBeTruthy();
expect(table.hasCapability('a', 'b-capability'))
.toBeFalsy();
expect(table.hasCapability('a', 'c-capability'))
.toBeFalsy();
expect(table.hasCapability('b', 'a-capability'))
.toBeFalsy();
expect(table.hasCapability('b', 'b-capability'))
.toBeTruthy();
expect(table.hasCapability('b', 'c-capability'))
.toBeFalsy();
});
});
}
);

View File

@@ -79,7 +79,7 @@ define(
expect(mockPolicyService.allow).toHaveBeenCalledWith(
'composition',
mockTypes[0],
mockDomainObjects[1]
mockTypes[1]
);
});

View File

@@ -24,94 +24,60 @@ define(
["../src/CompositionPolicy"],
function (CompositionPolicy) {
describe("Composition policy", function () {
var typeA,
typeB,
typeC,
mockChildObject,
var mockInjector,
mockTypeService,
mockCapabilityService,
mockTypes,
policy;
beforeEach(function () {
typeA = jasmine.createSpyObj(
'type A-- the particular kind',
['getKey', 'getDefinition']
mockInjector = jasmine.createSpyObj('$injector', ['get']);
mockTypeService = jasmine.createSpyObj(
'typeService',
['listTypes']
);
typeA.getKey.andReturn('a');
typeA.getDefinition.andReturn({
contains: ['a']
mockCapabilityService = jasmine.createSpyObj(
'capabilityService',
['getCapabilities']
);
// Both types can only contain b, let's say
mockTypes = ['a', 'b'].map(function (type) {
var mockType = jasmine.createSpyObj(
'type-' + type,
['getKey', 'getDefinition', 'getInitialModel']
);
mockType.getKey.andReturn(type);
mockType.getDefinition.andReturn({
contains: ['b']
});
mockType.getInitialModel.andReturn({});
return mockType;
});
typeB = jasmine.createSpyObj(
'type B-- anything goes',
['getKey', 'getDefinition']
);
typeB.getKey.andReturn('b');
typeB.getDefinition.andReturn({
contains: ['a', 'b']
mockInjector.get.andCallFake(function (name) {
return {
typeService: mockTypeService,
capabilityService: mockCapabilityService
}[name];
});
typeC = jasmine.createSpyObj(
'type C-- distinguishing and interested in telemetry',
['getKey', 'getDefinition']
);
typeC.getKey.andReturn('c');
typeC.getDefinition.andReturn({
contains: [{has: 'telemetry'}]
});
mockTypeService.listTypes.andReturn(mockTypes);
mockCapabilityService.getCapabilities.andReturn({});
mockChildObject = jasmine.createSpyObj(
'childObject',
['getCapability', 'hasCapability']
);
policy = new CompositionPolicy();
policy = new CompositionPolicy(mockInjector);
});
describe('enforces simple containment rules', function () {
it('allows when type matches', function () {
mockChildObject.getCapability.andReturn(typeA);
expect(policy.allow(typeA, mockChildObject))
.toBeTruthy();
expect(policy.allow(typeB, mockChildObject))
.toBeTruthy();
mockChildObject.getCapability.andReturn(typeB);
expect(policy.allow(typeB, mockChildObject))
.toBeTruthy();
});
it('disallows when type doesn\'t match', function () {
mockChildObject.getCapability.andReturn(typeB);
expect(policy.allow(typeA, mockChildObject))
.toBeFalsy();
mockChildObject.getCapability.andReturn(typeC);
expect(policy.allow(typeA, mockChildObject))
.toBeFalsy();
});
});
describe('enforces capability-based containment rules', function () {
it('allows when object has capability', function () {
mockChildObject.hasCapability.andReturn(true);
expect(policy.allow(typeC, mockChildObject))
.toBeTruthy();
expect(mockChildObject.hasCapability)
.toHaveBeenCalledWith('telemetry');
});
it('skips when object doesn\'t have capability', function () {
mockChildObject.hasCapability.andReturn(false);
expect(policy.allow(typeC, mockChildObject))
.toBeFalsy();
expect(mockChildObject.hasCapability)
.toHaveBeenCalledWith('telemetry');
});
// Test basic composition policy here; test more closely at
// the unit level in ContainmentTable for 'has' support, et al
it("enforces containment rules defined by types", function () {
expect(policy.allow(mockTypes[0], mockTypes[1]))
.toBeTruthy();
expect(policy.allow(mockTypes[1], mockTypes[1]))
.toBeTruthy();
expect(policy.allow(mockTypes[1], mockTypes[0]))
.toBeFalsy();
expect(policy.allow(mockTypes[0], mockTypes[0]))
.toBeFalsy();
});
});

View File

@@ -0,0 +1,96 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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/ContainmentTable"],
function (ContainmentTable) {
describe("Composition policy's containment table", function () {
var mockTypeService,
mockCapabilityService,
mockTypes,
table;
beforeEach(function () {
mockTypeService = jasmine.createSpyObj(
'typeService',
['listTypes']
);
mockCapabilityService = jasmine.createSpyObj(
'capabilityService',
['getCapabilities']
);
// Both types can only contain b, let's say
mockTypes = ['a', 'b', 'c'].map(function (type, index) {
var mockType = jasmine.createSpyObj(
'type-' + type,
['getKey', 'getDefinition', 'getInitialModel']
);
mockType.getKey.andReturn(type);
mockType.getDefinition.andReturn({
// First two contain objects with capability 'b';
// third one defines no containership rules
contains: (index < 2) ? [{ has: 'b' }] : undefined
});
// Return a model to drive apparent capabilities
mockType.getInitialModel.andReturn({ id: type });
return mockType;
});
mockTypeService.listTypes.andReturn(mockTypes);
mockCapabilityService.getCapabilities.andCallFake(function (model) {
var capabilities = {};
capabilities[model.id] = true;
return capabilities;
});
table = new ContainmentTable(
mockTypeService,
mockCapabilityService
);
});
// The plain type case is tested in CompositionPolicySpec,
// so just test for special syntax ('has', or no contains rules) here
it("enforces 'has' containment rules related to capabilities", function () {
expect(table.canContain(mockTypes[0], mockTypes[1]))
.toBeTruthy();
expect(table.canContain(mockTypes[1], mockTypes[1]))
.toBeTruthy();
expect(table.canContain(mockTypes[1], mockTypes[0]))
.toBeFalsy();
expect(table.canContain(mockTypes[0], mockTypes[0]))
.toBeFalsy();
});
it("allows anything when no containership rules are defined", function () {
expect(table.canContain(mockTypes[2], mockTypes[0]))
.toBeTruthy();
expect(table.canContain(mockTypes[2], mockTypes[1]))
.toBeTruthy();
expect(table.canContain(mockTypes[2], mockTypes[2]))
.toBeTruthy();
});
});
}
);

View File

@@ -241,7 +241,7 @@ define([
"property": "name",
"pattern": "\\S+",
"required": true,
"cssClass": "l-input-lg"
"cssclass": "l-input-lg"
},
{
"name": "Notes",
@@ -249,19 +249,19 @@ define([
"property": "notes",
"control": "textarea",
"required": false,
"cssClass": "l-textarea-sm"
"cssclass": "l-textarea-sm"
}
]
},
{
"key": "root",
"name": "Root",
"cssClass": "icon-folder"
"cssclass": "icon-folder"
},
{
"key": "folder",
"name": "Folder",
"cssClass": "icon-folder",
"cssclass": "icon-folder",
"features": "creation",
"description": "Create folders to organize other objects or links to objects.",
"priority": 1000,
@@ -272,11 +272,11 @@ define([
{
"key": "unknown",
"name": "Unknown Type",
"cssClass": "icon-object-unknown"
"cssclass": "icon-object-unknown"
},
{
"name": "Unknown Type",
"cssClass": "icon-object-unknown"
"cssclass": "icon-object-unknown"
}
],
"capabilities": [

View File

@@ -58,7 +58,7 @@ define(
* @property {string} key machine-readable identifier for this action
* @property {string} name human-readable name for this action
* @property {string} description human-readable description
* @property {string} cssClass CSS class for icon
* @property {string} cssclass CSS class for icon
* @property {ActionContext} context the context in which the action
* will be performed.
*/

View File

@@ -53,10 +53,10 @@ define(
*/
function CoreCapabilityProvider(capabilities, $log) {
// Filter by invoking the capability's appliesTo method
function filterCapabilities(model, id) {
function filterCapabilities(model) {
return capabilities.filter(function (capability) {
return capability.appliesTo ?
capability.appliesTo(model, id) :
capability.appliesTo(model) :
true;
});
}
@@ -75,8 +75,8 @@ define(
return result;
}
function getCapabilities(model, id) {
return packageCapabilities(filterCapabilities(model, id));
function getCapabilities(model) {
return packageCapabilities(filterCapabilities(model));
}
return {

View File

@@ -56,12 +56,12 @@ define(
* @method Type#getDescription
*/
/**
* Get the cssClass associated with this type. cssClass is a
* Get the cssclass associated with this type. cssclass is a
* string which will appear as an icon (when
* displayed in an appropriate font) which visually
* distinguish types from one another.
*
* @returns {string} the cssClass for this type
* @returns {string} the cssclass for this type
* @method Type#getCssClass
*/
/**
@@ -145,7 +145,7 @@ define(
};
TypeImpl.prototype.getCssClass = function () {
return this.typeDef.cssClass;
return this.typeDef.cssclass;
};
TypeImpl.prototype.getProperties = function () {

View File

@@ -33,7 +33,7 @@ define(
key: 'test-type',
name: 'Test Type',
description: 'A type, for testing',
cssClass: 'icon-telemetry-panel',
cssclass: 'icon-telemetry-panel',
inherits: ['test-parent-1', 'test-parent-2'],
features: ['test-feature-1'],
properties: [{}],

View File

@@ -30,18 +30,18 @@ define(
testTypeDefinitions = [
{
key: 'basic',
cssClass: "icon-magnify-in",
cssclass: "icon-magnify-in",
name: "Basic Type"
},
{
key: 'multi1',
cssClass: "icon-trash",
cssclass: "icon-trash",
description: "Multi1 Description",
capabilities: ['a1', 'b1']
},
{
key: 'multi2',
cssClass: "icon-magnify-out",
cssclass: "icon-magnify-out",
capabilities: ['a2', 'b2', 'c2']
},
{

View File

@@ -66,7 +66,7 @@ define([
"key": "move",
"name": "Move",
"description": "Move object to another location.",
"cssClass": "icon-move",
"cssclass": "icon-move",
"category": "contextual",
"implementation": MoveAction,
"depends": [
@@ -79,7 +79,7 @@ define([
"key": "copy",
"name": "Duplicate",
"description": "Duplicate object to another location.",
"cssClass": "icon-duplicate",
"cssclass": "icon-duplicate",
"category": "contextual",
"implementation": CopyAction,
"depends": [
@@ -95,7 +95,7 @@ define([
"key": "link",
"name": "Create Link",
"description": "Create Link to object in another location.",
"cssClass": "icon-link",
"cssclass": "icon-link",
"category": "contextual",
"implementation": LinkAction,
"depends": [
@@ -108,7 +108,7 @@ define([
"key": "follow",
"name": "Go To Original",
"description": "Go to the original, un-linked instance of this object.",
"cssClass": "",
"cssclass": "",
"category": "contextual",
"implementation": GoToOriginalAction
},
@@ -116,7 +116,7 @@ define([
"key": "locate",
"name": "Set Primary Location",
"description": "Set a domain object's primary location.",
"cssClass": "",
"cssclass": "",
"category": "contextual",
"implementation": SetPrimaryLocationAction
}

View File

@@ -48,7 +48,7 @@ define(
return this.policyService.allow(
"composition",
parentCandidate.getCapability('type'),
object
object.getCapability('type')
);
};

Some files were not shown because too many files have changed in this diff Show More