Merge remote-tracking branch 'github/mobile' into open74-merge-mobile

This commit is contained in:
Charles Hacskaylo 2015-08-26 16:55:15 -07:00
commit 1cbca41c53
295 changed files with 12542 additions and 9354 deletions
CONTRIBUTING.mdjsdoc.json
platform
commonUI
about/src
browse
dialog/src
edit
general
inspect/src
containment/src
core/src/actions

298
CONTRIBUTING.md Normal file

@ -0,0 +1,298 @@
# Contributing to Open MCT Web
This document describes the process of contributing to Open MCT Web as well
as the standards that will be applied when evaluating contributions.
Please be aware that additional agreements will be necessary before we can
accept changes from external contributors.
## Summary
The short version:
1. Write your contribution.
2. Make sure your contribution meets code, test, and commit message
standards as described below.
3. Submit a pull request from a topic branch back to `master`. Include a check
list, as described below. (Optionally, assign this to a specific member
for review.)
4. Respond to any discussion. When the reviewer decides it's ready, they
will merge back `master` and fill out their own check list.
## Contribution Process
Open MCT Web uses git for software version control, and for branching and
merging. The central repository is at
https://github.com/nasa/openmctweb.git.
### Roles
References to roles are made throughout this document. These are not intended
to reflect titles or long-term job assignments; rather, these are used as
descriptors to refer to members of the development team performing tasks in
the check-in process. These roles are:
* _Author_: The individual who has made changes to files in the software
repository, and wishes to check these in.
* _Reviewer_: The individual who reviews changes to files before they are
checked in.
* _Integrator_: The individual who performs the task of merging these files.
Usually the reviewer.
### Branching
Three basic types of branches may be included in the above repository:
1. Master branch.
2. Topic branches.
3. Developer branches.
Branches which do not fit into the above categories may be created and used
during the course of development for various reasons, such as large-scale
refactoring of code or implementation of complex features which may cause
instability. In these exceptional cases it is the responsibility of the
developer who initiates the task which motivated this branching to
communicate to the team the role of these branches and any associated
procedures for the duration of their use.
#### Master Branch
The role of the `master` branches is to represent the latest
"ready for test" version of the software. Source code on the master
branch has undergone peer review, and will undergo regular automated
testing with notification on failure. Master branches may be unstable
(particularly for recent features), but the intent is for the stability of
any features on master branches to be non-decreasing. It is the shared
responsibility of authors, reviewers, and integrators to ensure this.
#### Topic Branches
Topic branches are used by developers to perform and record work on issues.
Topic branches need not necessarily be stable, even when pushed to the
central repository; in fact, the practice of making incremental commits
while working on an issue and pushing these to the central repository is
encouraged, to avoid lost work and to share work-in-progress. (Small commits
also help isolate changes, which can help in identifying which change
introduced a defect, particularly when that defect went unnoticed for some
time, e.g. using `git bisect`.)
Topic branches should be named according to their corresponding issue
identifiers, all lower case, without hyphens. (e.g. branch mct9 would refer
to issue #9.)
In some cases, work on an issue may warrant the use of multiple divergent
branches; for instance, when a developer wants to try more than one solution
and compare them, or when a "dead end" is reached and an initial approach to
resolving an issue needs to be abandoned. In these cases, a short suffix
should be added to the additional branches; this may be simply a single
character (e.g. wtd481b) or, where useful, a descriptive term for what
distinguishes the branches (e.g. wtd481verbose). It is the responsibility of
the author to communicate which branch is intended to be merged to both the
reviewer and the integrator.
#### Developer Branches
Developer branches are any branches used for purposes outside of the scope
of the above; e.g. to try things out, or maintain a "my latest stuff"
branch that is not delayed by the review and integration process. These
may be pushed to the central repository, and may follow any naming convention
desired so long as the owner of the branch is identifiable, and so long as
the name chosen could not be mistaken for a topic or master branch.
### Merging
When development is complete on an issue, the first step toward merging it
back into the master branch is to file a Pull Request. The contributions
should meet code, test, and commit message standards as described below,
and the pull request should include a completed author checklist, also
as described below. Pull requests may be assigned to specific team
members when appropriate (e.g. to draw to a specific person's attention.)
Code review should take place using discussion features within the pull
request. When the reviewer is satisfied, they should add a comment to
the pull request containing the reviewer checklist (from below) and complete
the merge back to the master branch.
## Standards
Contributions to Open MCT Web are expected to meet the following standards.
In addition, reviewers should use general discretion before accepting
changes.
### Code Standards
JavaScript sources in Open MCT Web must satisfy JSLint under its default
settings. This is verified by the command line build.
#### Code Guidelines
JavaScript sources in Open MCT Web should:
* Use four spaces for indentation. Tabs should not be used.
* Include JSDoc for any exposed API (e.g. public methods, constructors.)
* Include non-JSDoc comments as-needed for explaining private variables,
methods, or algorithms when they are non-obvious.
* Define one public class per script, expressed as a constructor function
returned from an AMD-style module.
* Follow “Java-like” naming conventions. These includes:
* Classes should use camel case, first letter capitalized
(e.g. SomeClassName.)
* Methods, variables, fields, and function names should use camel case,
first letter lower-case (e.g. someVariableName.) Constants
(variables or fields which are meant to be declared and initialized
statically, and never changed) should use only capital letters, with
underscores between words (e.g. SOME_CONSTANT.)
* File name should be the name of the exported class, plus a .js extension
(e.g. SomeClassName.js)
* Avoid anonymous functions, except when functions are short (a few lines)
and/or their inclusion makes sense within the flow of the code
(e.g. as arguments to a forEach call.)
* Avoid deep nesting (especially of functions), except where necessary
(e.g. due to closure scope.)
* End with a single new-line character.
* Expose public methods by declaring them on the class's prototype.
* Within a given function's scope, do not mix declarations and imperative
code, and present these in the following order:
* First, variable declarations and initialization.
* Second, function declarations.
* Third, imperative statements.
* Finally, the returned value.
Deviations from Open MCT Web code style guidelines require two-party agreement,
typically from the author of the change and its reviewer.
#### Code Example
```js
/*global define*/
/**
* Bundles should declare themselves as namespaces in whichever source
* file is most like the "main point of entry" to the bundle.
* @namespace some/bundle
*/
define(
['./OtherClass'],
function (OtherClass) {
"use strict";
/**
* A summary of how to use this class goes here.
*
* @constructor
* @memberof some/bundle
*/
function ExampleClass() {
}
// Methods which are not intended for external use should
// not have JSDoc (or should be marked @private)
ExampleClass.prototype.privateMethod = function () {
};
/**
* A summary of this method goes here.
* @param {number} n a parameter
* @returns {number} a return value
*/
ExampleClass.prototype.publicMethod = function (n) {
return n * 2;
}
return ExampleClass;
}
);
```
### Test Standards
Automated testing shall occur whenever changes are merged into the main
development branch and must be confirmed alongside any pull request.
Automated tests are typically unit tests which exercise individual software
components. Tests are subject to code review along with the actual
implementation, to ensure that tests are applicable and useful.
Examples of useful tests:
* Tests which replicate bugs (or their root causes) to verify their
resolution.
* Tests which reflect details from software specifications.
* Tests which exercise edge or corner cases among inputs.
* Tests which verify expected interactions with other components in the
system.
During automated testing, code coverage metrics will be reported. Line
coverage must remain at or above 80%.
### Commit Message Standards
Commit messages should:
* Contain a one-line subject, followed by one line of white space,
followed by one or more descriptive paragraphs, each separated by one
line of white space.
* Contain a short (usually one word) reference to the feature or subsystem
the commit effects, in square brackets, at the start of the subject line
(e.g. `[Documentation] Draft of check-in process`)
* Contain a reference to a relevant issue number in the body of the commit.
* This is important for traceability; while branch names also provide this,
you cannot tell from looking at a commit what branch it was authored on.
* Describe the change that was made, and any useful rationale therefore.
* Comments in code should explain what things do, commit messages describe
how they came to be done that way.
* Provide sufficient information for a reviewer to understand the changes
made and their relationship to previous code.
Commit messages should not:
* Exceed 54 characters in length on the subject line.
* Exceed 72 characters in length in the body of the commit.
* Except where necessary to maintain the structure of machine-readable or
machine-generated text (e.g. error messages)
See [Contributing to a Project](http://git-scm.com/book/ch5-2.html) from
Pro Git by Shawn Chacon and Ben Straub for a bit of the rationale behind
these standards.
## Issue Reporting
Issues are tracked at https://github.com/nasa/openmctweb/issues
Issues should include:
* A short description of the issue encountered.
* A longer-form description of the issue encountered. When possible, steps to
reproduce the issue.
* When possible, a description of the impact of the issue. What use case does
it impede?
* An assessment of the severity of the issue.
Issue severity is categorized as follows (in ascending order):
* _Trivial_: Minimal impact on the usefulness and functionality of the
software; a "nice-to-have."
* _(Unspecified)_: Major loss of functionality or impairment of use.
* _Critical_: Large-scale loss of functionality or impairment of use,
such that remaining utility becomes marginal.
* _Blocker_: Harmful or otherwise unacceptable behavior. Must fix.
## Check Lists
The following check lists should be completed and attached to pull requests
when they are filed (author checklist) and when they are merged (reviewer
checklist.)
### Author Checklist
1. Changes address original issue?
2. Unit tests included and/or updated with changes?
3. Command line build passes?
4. Expect to pass code review?
### Reviewer Checklist
1. Changes appear to address issue?
2. Appropriate unit tests included?
3. Code style and in-line documentation are appropriate?
4. Commit messages meet standards?

@ -1,10 +1,12 @@
{
"source": {
"include": [
"example/",
"platform/"
],
"includePattern": "(example|platform)/.+\\.js$",
"includePattern": "platform/.+\\.js$",
"excludePattern": ".+\\Spec\\.js$|lib/.+"
}
}
},
"plugins": [
"plugins/markdown"
]
}

@ -21,6 +21,11 @@
*****************************************************************************/
/*global define*/
/**
* Implements Open MCT Web's About dialog.
* @namespace platform/commonUI/about
*/
define(
[],
function () {
@ -29,35 +34,36 @@ define(
/**
* The AboutController provides information to populate the
* About dialog.
* @memberof platform/commonUI/about
* @constructor
* @param {object[]} versions an array of version extensions;
* injected from `versions[]`
* @param $window Angular-injected window object
*/
function AboutController(versions, $window) {
return {
/**
* Get version info. This is given as an array of
* objects, where each object is intended to appear
* as a line-item in the version information listing.
* @memberof AboutController#
* @returns {object[]} version information
*/
versions: function () {
return versions;
},
/**
* Open a new window (or tab, depending on browser
* configuration) containing open source licenses.
* @memberof AboutController#
*/
openLicenses: function () {
// Open a new browser window at the licenses route
$window.open("#/licenses");
}
};
this.versionDefinitions = versions;
this.$window = $window;
}
/**
* Get version info. This is given as an array of
* objects, where each object is intended to appear
* as a line-item in the version information listing.
* @returns {object[]} version information
*/
AboutController.prototype.versions = function () {
return this.versionDefinitions;
};
/**
* Open a new window (or tab, depending on browser
* configuration) containing open source licenses.
*/
AboutController.prototype.openLicenses = function () {
// Open a new browser window at the licenses route
this.$window.open("#/licenses");
};
return AboutController;
}
);
);

@ -29,20 +29,22 @@ define(
/**
* Provides extension-introduced licenses information to the
* licenses route.
* @memberof platform/commonUI/about
* @constructor
*/
function LicenseController(licenses) {
return {
/**
* Get license information.
* @returns {Array} license extensions
*/
licenses: function () {
return licenses;
}
};
this.licenseDefinitions = licenses;
}
/**
* Get license information.
* @returns {Array} license extensions
* @memberof platform/commonUI/about.LicenseController#
*/
LicenseController.prototype.licenses = function () {
return this.licenseDefinitions;
};
return LicenseController;
}
);
);

@ -29,21 +29,23 @@ define(
/**
* The LogoController provides functionality to the application
* logo in the bottom-right of the user interface.
* @memberof platform/commonUI/about
* @constructor
* @param {OverlayService} overlayService the overlay service
*/
function LogoController(overlayService) {
return {
/**
* Display the About dialog.
* @memberof LogoController#
*/
showAboutDialog: function () {
overlayService.createOverlay("overlay-about");
}
};
this.overlayService = overlayService;
}
/**
* Display the About dialog.
* @memberof LogoController#
* @memberof platform/commonUI/about.LogoController#
*/
LogoController.prototype.showAboutDialog = function () {
this.overlayService.createOverlay("overlay-about");
};
return LogoController;
}
);
);

@ -25,14 +25,20 @@
<div class="holder browse-area s-browse-area abs browse-wrapper" ng-class="treeClass ? 'browse-showtree' : 'browse-hidetree'">
<mct-split-pane class='contents abs' anchor='left'>
<div class='split-pane-component treeview pane left'>
<div class="holder abs l-mobile">
<div class='tree-holder abs mobile-tree-holder'>
<mct-representation key="'tree'"
mct-object="domainObject"
ng-model="treeModel">
</mct-representation>
</div>
<mct-representation key="'create-button'" mct-object="navigatedObject">
<mct-representation key="'create-button'" mct-object="navigatedObject">
</mct-representation>
<div class='holder search-holder abs l-mobile'
ng-class="{active: treeModel.search}">
<mct-representation key="'search'"
mct-object="domainObject"
ng-model="treeModel">
</mct-representation>
</div>
<div class='tree-holder abs mobile-tree-holder'
ng-hide="treeModel.search">
<mct-representation key="'tree'"
mct-object="domainObject"
ng-model="treeModel">
</mct-representation>
</div>
</div>

@ -22,7 +22,8 @@
/*global define,Promise*/
/**
* Module defining BrowseController. Created by vwoeltje on 11/7/14.
* This bundle implements Browse mode.
* @namespace platform/commonUI/browse
*/
define(
[],
@ -39,6 +40,7 @@ define(
* which Angular templates first have access to the domain object
* hierarchy.
*
* @memberof platform/commonUI/browse
* @constructor
*/
function BrowseController($scope, $route, $location, objectService, navigationService, urlService) {
@ -187,6 +189,12 @@ define(
navigationService.removeListener(setNavigation);
});
// If the user has selected an object (and is portrait
// on a phone), then hide the tree menu
$scope.$on("select-obj", function () {
$scope.treeSlide();
});
$scope.backArrow = navigateToParent;
$scope.checkRoot = checkRoot;
@ -196,3 +204,4 @@ define(
return BrowseController;
}
);

@ -29,6 +29,7 @@ define(
/**
* Controller for the `browse-object` representation of a domain
* object (the right-hand side of Browse mode.)
* @memberof platform/commonUI/browse
* @constructor
*/
function BrowseObjectController($scope, $location, $route) {
@ -71,3 +72,4 @@ define(
return BrowseObjectController;
}
);

@ -33,19 +33,29 @@ define(
* A left-click on the menu arrow should display a
* context menu. This controller launches the context
* menu.
* @memberof platform/commonUI/browse
* @constructor
*/
function MenuArrowController($scope) {
function showMenu(event) {
var actionContext = {key: 'menu', domainObject: $scope.domainObject, event: event};
$scope.domainObject.getCapability('action').perform(actionContext);
}
return {
showMenu: showMenu
};
this.$scope = $scope;
}
/**
* Show a context menu for the domain object in this scope.
*
* @param event the browser event which caused this (used to
* position the menu)
*/
MenuArrowController.prototype.showMenu = function (event) {
var actionContext = {
key: 'menu',
domainObject: this.$scope.domainObject,
event: event
};
this.$scope.domainObject.getCapability('action').perform(actionContext);
};
return MenuArrowController;
}
);
);

@ -34,7 +34,10 @@ define(
* domain objects of a specific type. This is the action that
* is performed when a user uses the Create menu.
*
* @memberof platform/commonUI/browse
* @implements {Action}
* @constructor
*
* @param {Type} type the type of domain object to create
* @param {DomainObject} parent the domain object that should
* act as a container for the newly-created object
@ -49,78 +52,84 @@ define(
* of the newly-created domain object
*/
function CreateAction(type, parent, context, dialogService, creationService, policyService) {
this.metadata = {
key: 'create',
glyph: type.getGlyph(),
name: type.getName(),
type: type.getKey(),
description: type.getDescription(),
context: context
};
this.type = type;
this.parent = parent;
this.policyService = policyService;
this.dialogService = dialogService;
this.creationService = creationService;
}
/**
* Create a new object of the given type.
* This will prompt for user input first.
*/
CreateAction.prototype.perform = function () {
/*
Overview of steps in object creation:
1. Show dialog
a. Prepare dialog contents
b. Invoke dialogService
a. Prepare dialog contents
b. Invoke dialogService
2. Create new object in persistence service
a. Generate UUID
b. Store model
a. Generate UUID
b. Store model
3. Mutate destination container
a. Get mutation capability
b. Add new id to composition
a. Get mutation capability
b. Add new id to composition
4. Persist destination container
a. ...use persistence capability.
a. ...use persistence capability.
*/
function perform() {
// The wizard will handle creating the form model based
// on the type...
var wizard = new CreateWizard(type, parent, policyService);
// The wizard will handle creating the form model based
// on the type...
var wizard =
new CreateWizard(this.type, this.parent, this.policyService),
self = this;
// Create and persist the new object, based on user
// input.
function persistResult(formValue) {
var parent = wizard.getLocation(formValue),
newModel = wizard.createModel(formValue);
return creationService.createObject(newModel, parent);
}
function doNothing() {
// Create cancelled, do nothing
return false;
}
return dialogService.getUserInput(
wizard.getFormStructure(),
wizard.getInitialFormValue()
).then(persistResult, doNothing);
// Create and persist the new object, based on user
// input.
function persistResult(formValue) {
var parent = wizard.getLocation(formValue),
newModel = wizard.createModel(formValue);
return self.creationService.createObject(newModel, parent);
}
return {
/**
* Create a new object of the given type.
* This will prompt for user input first.
* @method
* @memberof CreateAction
*/
perform: perform,
function doNothing() {
// Create cancelled, do nothing
return false;
}
/**
* Get metadata about this action. This includes fields:
* * `name`: Human-readable name
* * `key`: Machine-readable identifier ("create")
* * `glyph`: Glyph to use as an icon for this action
* * `description`: Human-readable description
* * `context`: The context in which this action will be performed.
*
* @return {object} metadata about the create action
*/
getMetadata: function () {
return {
key: 'create',
glyph: type.getGlyph(),
name: type.getName(),
type: type.getKey(),
description: type.getDescription(),
context: context
};
}
};
}
return this.dialogService.getUserInput(
wizard.getFormStructure(),
wizard.getInitialFormValue()
).then(persistResult, doNothing);
};
/**
* Metadata associated with a Create action.
* @typedef {ActionMetadata} CreateActionMetadata
* @property {string} type the key for the type of domain object
* to be created
*/
/**
* Get metadata about this action.
* @returns {CreateActionMetadata} metadata about this action
*/
CreateAction.prototype.getMetadata = function () {
return this.metadata;
};
return CreateAction;
}
);
);

@ -33,7 +33,10 @@ define(
* The CreateActionProvider is an ActionProvider which introduces
* a Create action for each creatable domain object type.
*
* @memberof platform/commonUI/browse
* @constructor
* @implements {ActionService}
*
* @param {TypeService} typeService the type service, used to discover
* available types
* @param {DialogService} dialogService the dialog service, used by
@ -44,44 +47,41 @@ define(
* object creation.
*/
function CreateActionProvider(typeService, dialogService, creationService, policyService) {
return {
/**
* Get all Create actions which are applicable in the provided
* context.
* @memberof CreateActionProvider
* @method
* @returns {CreateAction[]}
*/
getActions: function (actionContext) {
var context = actionContext || {},
key = context.key,
destination = context.domainObject;
// We only provide Create actions, and we need a
// domain object to serve as the container for the
// newly-created object (although the user may later
// make a different selection)
if (key !== 'create' || !destination) {
return [];
}
// Introduce one create action per type
return typeService.listTypes().filter(function (type) {
return type.hasFeature("creation");
}).map(function (type) {
return new CreateAction(
type,
destination,
context,
dialogService,
creationService,
policyService
);
});
}
};
this.typeService = typeService;
this.dialogService = dialogService;
this.creationService = creationService;
this.policyService = policyService;
}
CreateActionProvider.prototype.getActions = function (actionContext) {
var context = actionContext || {},
key = context.key,
destination = context.domainObject,
self = this;
// We only provide Create actions, and we need a
// domain object to serve as the container for the
// newly-created object (although the user may later
// make a different selection)
if (key !== 'create' || !destination) {
return [];
}
// Introduce one create action per type
return this.typeService.listTypes().filter(function (type) {
return type.hasFeature("creation");
}).map(function (type) {
return new CreateAction(
type,
destination,
context,
self.dialogService,
self.creationService,
self.policyService
);
});
};
return CreateActionProvider;
}
);
);

@ -34,6 +34,7 @@ define(
* set of Create actions based on the currently-selected
* domain object.
*
* @memberof platform/commonUI/browse
* @constructor
*/
function CreateMenuController($scope) {
@ -55,4 +56,4 @@ define(
return CreateMenuController;
}
);
);

@ -21,12 +21,6 @@
*****************************************************************************/
/*global define*/
/**
* Defines the CreateWizard, used by the CreateAction to
* populate the form shown in dialog based on the created type.
*
* @module core/action/create-wizard
*/
define(
function () {
'use strict';
@ -37,113 +31,118 @@ define(
* @param {TypeImpl} type the type of domain object to be created
* @param {DomainObject} parent the domain object to serve as
* the initial parent for the created object, in the dialog
* @memberof platform/commonUI/browse
* @constructor
* @memberof module:core/action/create-wizard
*/
function CreateWizard(type, parent, policyService) {
var model = type.getInitialModel(),
properties = type.getProperties();
this.type = type;
this.model = type.getInitialModel();
this.properties = type.getProperties();
this.parent = parent;
this.policyService = policyService;
}
/**
* Get the form model for this wizard; this is a description
* that will be rendered to an HTML form. See the
* platform/forms bundle
*
* @return {FormModel} formModel the form model to
* show in the create dialog
*/
CreateWizard.prototype.getFormStructure = function () {
var sections = [],
type = this.type,
policyService = this.policyService;
function validateLocation(locatingObject) {
var locatingType = locatingObject &&
locatingObject.getCapability('type');
locatingObject.getCapability('type');
return locatingType && policyService.allow(
"composition",
locatingType,
type
);
"composition",
locatingType,
type
);
}
sections.push({
name: "Properties",
rows: this.properties.map(function (property, index) {
// Property definition is same as form row definition
var row = Object.create(property.getDefinition());
// Use index as the key into the formValue;
// this correlates to the indexing provided by
// getInitialFormValue
row.key = index;
return row;
})
});
// Ensure there is always a "save in" section
sections.push({ name: 'Location', rows: [{
name: "Save In",
control: "locator",
validate: validateLocation,
key: "createParent"
}]});
return {
/**
* Get the form model for this wizard; this is a description
* that will be rendered to an HTML form. See the
* platform/forms bundle
*
* @return {FormModel} formModel the form model to
* show in the create dialog
*/
getFormStructure: function () {
var sections = [];
sections.push({
name: "Properties",
rows: properties.map(function (property, index) {
// Property definition is same as form row definition
var row = Object.create(property.getDefinition());
// Use index as the key into the formValue;
// this correlates to the indexing provided by
// getInitialFormValue
row.key = index;
return row;
})
});
// Ensure there is always a "save in" section
sections.push({ name: 'Location', rows: [{
name: "Save In",
control: "locator",
validate: validateLocation,
key: "createParent"
}]});
return {
sections: sections,
name: "Create a New " + type.getName()
};
},
/**
* Get the initial value for the form being described.
* This will include the values for all properties described
* in the structure.
*
* @returns {object} the initial value of the form
*/
getInitialFormValue: function () {
// Start with initial values for properties
var formValue = properties.map(function (property) {
return property.getValue(model);
});
// Include the createParent
formValue.createParent = parent;
return formValue;
},
/**
* Based on a populated form, get the domain object which
* should be used as a parent for the newly-created object.
* @return {DomainObject}
*/
getLocation: function (formValue) {
return formValue.createParent || parent;
},
/**
* Create the domain object model for a newly-created object,
* based on user input read from a formModel.
* @return {object} the domain object' model
*/
createModel: function (formValue) {
// Clone
var newModel = JSON.parse(JSON.stringify(model));
// Always use the type from the type definition
newModel.type = type.getKey();
// Update all properties
properties.forEach(function (property, index) {
property.setValue(newModel, formValue[index]);
});
return newModel;
}
sections: sections,
name: "Create a New " + this.type.getName()
};
};
/**
* Get the initial value for the form being described.
* This will include the values for all properties described
* in the structure.
*
* @returns {object} the initial value of the form
*/
CreateWizard.prototype.getInitialFormValue = function () {
// Start with initial values for properties
var model = this.model,
formValue = this.properties.map(function (property) {
return property.getValue(model);
});
}
// Include the createParent
formValue.createParent = this.parent;
return formValue;
};
/**
* Based on a populated form, get the domain object which
* should be used as a parent for the newly-created object.
* @return {DomainObject}
*/
CreateWizard.prototype.getLocation = function (formValue) {
return formValue.createParent || this.parent;
};
/**
* Create the domain object model for a newly-created object,
* based on user input read from a formModel.
* @return {object} the domain object model
*/
CreateWizard.prototype.createModel = function (formValue) {
// Clone
var newModel = JSON.parse(JSON.stringify(this.model));
// Always use the type from the type definition
newModel.type = this.type.getKey();
// Update all properties
this.properties.forEach(function (property, index) {
property.setValue(newModel, formValue[index]);
});
return newModel;
};
return CreateWizard;
}
);
);

@ -39,15 +39,45 @@ define(
* persisting new domain objects. Handles all actual object
* mutation and persistence associated with domain object
* creation.
* @memberof platform/commonUI/browse
* @constructor
*/
function CreationService(persistenceService, $q, $log) {
this.persistenceService = persistenceService;
this.$q = $q;
this.$log = $log;
}
/**
* Create a new domain object with the provided model, as
* a member of the provided parent domain object's composition.
* This parent will additionally determine which persistence
* space an object is created within (as it is possible to
* have multiple persistence spaces attached.)
*
* @param {object} model the model for the newly-created
* domain object
* @param {DomainObject} parent the domain object which
* should contain the newly-created domain object
* in its composition
* @return {Promise} a promise that will resolve when the domain
* object has been created
*/
CreationService.prototype.createObject = function (model, parent) {
var persistence = parent.getCapability("persistence"),
self = this;
// Store the location of an object relative to it's parent.
function addLocationToModel(modelId, model, parent) {
model.location = parent.getId();
return model;
}
// Persist the new domain object's model; it will be fully
// constituted as a domain object when loaded back, as all
// domain object models are.
function doPersist(space, id, model) {
return persistenceService.createObject(
return self.persistenceService.createObject(
space,
id,
model
@ -66,14 +96,14 @@ define(
}
} else {
// This is abnormal; composition should be an array
$log.warn(NO_COMPOSITION_WARNING + parent.getId());
self.$log.warn(NO_COMPOSITION_WARNING + parent.getId());
return false; // Cancel mutation
}
});
return $q.when(mutatationResult).then(function (result) {
return self.$q.when(mutatationResult).then(function (result) {
if (!result) {
$log.error("Could not mutate " + parent.getId());
self.$log.error("Could not mutate " + parent.getId());
return undefined;
}
@ -93,56 +123,28 @@ define(
});
}
// Store the location of an object relative to it's parent.
function addLocationToModel(modelId, model, parent) {
model.location = parent.getId();
return model;
// We need the parent's persistence capability to determine
// what space to create the new object's model in.
if (!persistence) {
self.$log.warn(NON_PERSISTENT_WARNING);
return self.$q.reject(new Error(NON_PERSISTENT_WARNING));
}
// Create a new domain object with the provided model as a
// member of the specified parent's composition
function createObject(model, parent) {
var persistence = parent.getCapability("persistence");
// We create a new domain object in three sequential steps:
// 1. Get a new UUID for the object
// 2. Create a model with that ID in the persistence space
// 3. Add that ID to
return self.$q.when(uuid()).then(function (id) {
model = addLocationToModel(id, model, parent);
return doPersist(persistence.getSpace(), id, model);
}).then(function (id) {
return addToComposition(id, parent, persistence);
});
};
// We need the parent's persistence capability to determine
// what space to create the new object's model in.
if (!persistence) {
$log.warn(NON_PERSISTENT_WARNING);
return $q.reject(new Error(NON_PERSISTENT_WARNING));
}
// We create a new domain object in three sequential steps:
// 1. Get a new UUID for the object
// 2. Create a model with that ID in the persistence space
// 3. Add that ID to
return $q.when(
uuid()
).then(function (id) {
model = addLocationToModel(id, model, parent);
return doPersist(persistence.getSpace(), id, model);
}).then(function (id) {
return addToComposition(id, parent, persistence);
});
}
return {
/**
* Create a new domain object with the provided model, as
* a member of the provided parent domain object's composition.
* This parent will additionally determine which persistence
* space an object is created within (as it is possible to
* have multiple persistence spaces attached.)
*
* @param {object} model the model for the newly-created
* domain object
* @param {DomainObject} parent the domain object which
* should contain the newly-created domain object
* in its composition
*/
createObject: createObject
};
}
return CreationService;
}
);

@ -30,6 +30,7 @@ define(
* Controller for the "locator" control, which provides the
* user with the ability to select a domain object as the
* destination for a newly-created object in the Create menu.
* @memberof platform/commonUI/browse
* @constructor
*/
function LocatorController($scope) {
@ -79,3 +80,4 @@ define(
return LocatorController;
}
);

@ -31,32 +31,34 @@ define(
/**
* The navigate action navigates to a specific domain object.
* @memberof platform/commonUI/browse
* @constructor
* @implements {Action}
*/
function NavigateAction(navigationService, $q, context) {
var domainObject = context.domainObject;
function perform() {
// Set navigation, and wrap like a promise
return $q.when(navigationService.setNavigation(domainObject));
}
return {
/**
* Navigate to the object described in the context.
* @returns {Promise} a promise that is resolved once the
* navigation has been updated
*/
perform: perform
};
this.domainObject = context.domainObject;
this.$q = $q;
this.navigationService = navigationService;
}
/**
* Navigate to the object described in the context.
* @returns {Promise} a promise that is resolved once the
* navigation has been updated
*/
NavigateAction.prototype.perform = function () {
// Set navigation, and wrap like a promise
return this.$q.when(
this.navigationService.setNavigation(this.domainObject)
);
};
/**
* Navigate as an action is only applicable when a domain object
* is described in the action context.
* @param {ActionContext} context the context in which the action
* will be performed
* @returns true if applicable
* @returns {boolean} true if applicable
*/
NavigateAction.appliesTo = function (context) {
return context.domainObject !== undefined;
@ -64,4 +66,4 @@ define(
return NavigateAction;
}
);
);

@ -32,68 +32,58 @@ define(
/**
* The navigation service maintains the application's current
* navigation state, and allows listening for changes thereto.
* @memberof platform/commonUI/browse
* @constructor
*/
function NavigationService() {
var navigated,
callbacks = [];
this.navigated = undefined;
this.callbacks = [];
}
// Getter for current navigation
function getNavigation() {
return navigated;
}
/**
* Get the current navigation state.
* @returns {DomainObject} the object that is navigated-to
*/
NavigationService.prototype.getNavigation = function () {
return this.navigated;
};
// Setter for navigation; invokes callbacks
function setNavigation(value) {
if (navigated !== value) {
navigated = value;
callbacks.forEach(function (callback) {
callback(value);
});
}
}
// Adds a callback
function addListener(callback) {
callbacks.push(callback);
}
// Filters out a callback
function removeListener(callback) {
callbacks = callbacks.filter(function (cb) {
return cb !== callback;
/**
* Set the current navigation state. This will invoke listeners.
* @param {DomainObject} domainObject the domain object to navigate to
*/
NavigationService.prototype.setNavigation = function (value) {
if (this.navigated !== value) {
this.navigated = value;
this.callbacks.forEach(function (callback) {
callback(value);
});
}
};
return {
/**
* Get the current navigation state.
*/
getNavigation: getNavigation,
/**
* Set the current navigation state. Thiswill invoke listeners.
* @param {DomainObject} value the domain object to navigate
* to
*/
setNavigation: setNavigation,
/**
* 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
*/
addListener: addListener,
/**
* Stop listening for changes in navigation state.
* @param {function} callback the callback which should
* no longer be invoked when navigation state
* changes
*/
removeListener: removeListener
};
}
/**
* 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
*/
NavigationService.prototype.addListener = function (callback) {
this.callbacks.push(callback);
};
/**
* Stop listening for changes in navigation state.
* @param {function} callback the callback which should
* no longer be invoked when navigation state
* changes
*/
NavigationService.prototype.removeListener = function (callback) {
this.callbacks = this.callbacks.filter(function (cb) {
return cb !== callback;
});
};
return NavigationService;
}
);
);

@ -35,36 +35,32 @@ define(
/**
* The fullscreen action toggles between fullscreen display
* and regular in-window display.
* @memberof platform/commonUI/browse
* @constructor
* @implements {Action}
*/
function FullscreenAction(context) {
return {
/**
* Toggle full screen state
*/
perform: function () {
screenfull.toggle();
},
/**
* Get metadata about this action, including the
* applicable glyph to display.
*/
getMetadata: function () {
// We override getMetadata, because the glyph 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.glyph = screenfull.isFullscreen ? "_" : "z";
metadata.description = screenfull.isFullscreen ?
EXIT_FULLSCREEN : ENTER_FULLSCREEN;
metadata.group = "windowing";
metadata.context = context;
return metadata;
}
};
this.context = context;
}
FullscreenAction.prototype.perform = function () {
screenfull.toggle();
};
FullscreenAction.prototype.getMetadata = function () {
// We override getMetadata, because the glyph 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.glyph = screenfull.isFullscreen ? "_" : "z";
metadata.description = screenfull.isFullscreen ?
EXIT_FULLSCREEN : ENTER_FULLSCREEN;
metadata.group = "windowing";
metadata.context = this.context;
return metadata;
};
return FullscreenAction;
}
);
);

@ -33,35 +33,29 @@ define(
/**
* The new tab action allows a domain object to be opened
* into a new browser tab.
* @memberof platform/commonUI/browse
* @constructor
* @implements {Action}
*/
function NewTabAction(urlService, $window, context) {
// Returns the selected domain object
// when using the context menu or the top right button
// based on the context and the existance of the object
// It is set to object an returned
function getSelectedObject() {
var object;
if (context.selectedObject) {
object = context.selectedObject;
} else {
object = context.domainObject;
}
return object;
}
return {
// Performs the open in new tab function
// By calling the url service, the mode needed
// (browse) and the domainObject is passed in and
// the path is returned and opened in a new tab
perform: function () {
$window.open(urlService.urlForNewTab("browse", getSelectedObject()),
"_blank");
}
context = context || {};
this.urlService = urlService;
this.open = function () {
$window.open.apply($window, arguments);
};
// Choose the object to be opened into a new tab
this.domainObject = context.selectedObject || context.domainObject;
}
NewTabAction.prototype.perform = function () {
this.open(
this.urlService.urlForNewTab("browse", this.domainObject),
"_blank"
);
};
return NewTabAction;
}
);
);

@ -29,6 +29,7 @@ define(
/**
* Updates the title of the current window to reflect the name
* of the currently navigated-to domain object.
* @memberof platform/commonUI/browse
* @constructor
*/
function WindowTitler(navigationService, $rootScope, $document) {
@ -49,4 +50,4 @@ define(
return WindowTitler;
}
);
);

@ -164,7 +164,12 @@ define(
// lets the html code know if the
// tree menu is open.
it("calls the treeSlide function", function () {
mockScope.treeSlide();
expect(mockScope.$on).toHaveBeenCalledWith(
"select-obj",
jasmine.any(Function)
);
mockScope.$on.calls[1].args[1]();
});
it("releases its navigation listener when its scope is destroyed", function () {
@ -172,7 +177,8 @@ define(
"$destroy",
jasmine.any(Function)
);
mockScope.$on.mostRecentCall.args[1]();
mockScope.$on.calls[0].args[1]();
// Should remove the listener it added earlier
expect(mockNavigationService.removeListener).toHaveBeenCalledWith(
mockNavigationService.addListener.mostRecentCall.args[0]

@ -22,7 +22,9 @@
/*global define*/
/**
* Module defining DialogService. Created by vwoeltje on 11/10/14.
* This bundle implements the dialog service, which can be used to
* launch dialogs for user input & notifications.
* @namespace platform/commonUI/dialog
*/
define(
[],
@ -32,128 +34,130 @@ define(
* The dialog service is responsible for handling window-modal
* communication with the user, such as displaying forms for user
* input.
* @memberof platform/commonUI/dialog
* @constructor
*/
function DialogService(overlayService, $q, $log) {
var overlay,
dialogVisible = false;
// Stop showing whatever overlay is currently active
// (e.g. because the user hit cancel)
function dismiss() {
if (overlay) {
overlay.dismiss();
}
dialogVisible = false;
}
function getDialogResponse(key, model, resultGetter) {
// We will return this result as a promise, because user
// input is asynchronous.
var deferred = $q.defer(),
overlayModel;
// Confirm function; this will be passed in to the
// overlay-dialog template and associated with a
// OK button click
function confirm(value) {
// Pass along the result
deferred.resolve(resultGetter ? resultGetter() : value);
// Stop showing the dialog
dismiss();
}
// Cancel function; this will be passed in to the
// overlay-dialog template and associated with a
// Cancel or X button click
function cancel() {
deferred.reject();
dismiss();
}
// Add confirm/cancel callbacks
model.confirm = confirm;
model.cancel = cancel;
if (dialogVisible) {
// Only one dialog should be shown at a time.
// The application design should be such that
// we never even try to do this.
$log.warn([
"Dialog already showing; ",
"unable to show ",
model.name
].join(""));
deferred.reject();
} else {
// Add the overlay using the OverlayService, which
// will handle actual insertion into the DOM
overlay = overlayService.createOverlay(
key,
model
);
// Track that a dialog is already visible, to
// avoid spawning multiple dialogs at once.
dialogVisible = true;
}
return deferred.promise;
}
function getUserInput(formModel, value) {
var overlayModel = {
title: formModel.name,
message: formModel.message,
structure: formModel,
value: value
};
// Provide result from the model
function resultGetter() {
return overlayModel.value;
}
// Show the overlay-dialog
return getDialogResponse(
"overlay-dialog",
overlayModel,
resultGetter
);
}
function getUserChoice(dialogModel) {
// Show the overlay-options dialog
return getDialogResponse(
"overlay-options",
{ dialog: dialogModel }
);
}
return {
/**
* Request user input via a window-modal dialog.
*
* @param {FormModel} formModel a description of the form
* to be shown (see platform/forms)
* @param {object} value the initial state of the form
* @returns {Promise} a promsie for the form value that the
* user has supplied; this may be rejected if
* user input cannot be obtained (for instance,
* because the user cancelled the dialog)
*/
getUserInput: getUserInput,
/**
* Request that the user chooses from a set of options,
* which will be shown as buttons.
*
* @param dialogModel a description of the dialog to show
*/
getUserChoice: getUserChoice
};
this.overlayService = overlayService;
this.$q = $q;
this.$log = $log;
this.overlay = undefined;
this.dialogVisible = false;
}
// Stop showing whatever overlay is currently active
// (e.g. because the user hit cancel)
DialogService.prototype.dismiss = function () {
var overlay = this.overlay;
if (overlay) {
overlay.dismiss();
}
this.dialogVisible = false;
};
DialogService.prototype.getDialogResponse = function (key, model, resultGetter) {
// We will return this result as a promise, because user
// input is asynchronous.
var deferred = this.$q.defer(),
self = this;
// Confirm function; this will be passed in to the
// overlay-dialog template and associated with a
// OK button click
function confirm(value) {
// Pass along the result
deferred.resolve(resultGetter ? resultGetter() : value);
// Stop showing the dialog
self.dismiss();
}
// Cancel function; this will be passed in to the
// overlay-dialog template and associated with a
// Cancel or X button click
function cancel() {
deferred.reject();
self.dismiss();
}
// Add confirm/cancel callbacks
model.confirm = confirm;
model.cancel = cancel;
if (this.dialogVisible) {
// Only one dialog should be shown at a time.
// The application design should be such that
// we never even try to do this.
this.$log.warn([
"Dialog already showing; ",
"unable to show ",
model.name
].join(""));
deferred.reject();
} else {
// Add the overlay using the OverlayService, which
// will handle actual insertion into the DOM
this.overlay = this.overlayService.createOverlay(
key,
model
);
// Track that a dialog is already visible, to
// avoid spawning multiple dialogs at once.
this.dialogVisible = true;
}
return deferred.promise;
};
/**
* Request user input via a window-modal dialog.
*
* @param {FormModel} formModel a description of the form
* to be shown (see platform/forms)
* @param {object} value the initial state of the form
* @returns {Promise} a promise for the form value that the
* user has supplied; this may be rejected if
* user input cannot be obtained (for instance,
* because the user cancelled the dialog)
*/
DialogService.prototype.getUserInput = function (formModel, value) {
var overlayModel = {
title: formModel.name,
message: formModel.message,
structure: formModel,
value: value
};
// Provide result from the model
function resultGetter() {
return overlayModel.value;
}
// Show the overlay-dialog
return this.getDialogResponse(
"overlay-dialog",
overlayModel,
resultGetter
);
};
/**
* Request that the user chooses from a set of options,
* which will be shown as buttons.
*
* @param dialogModel a description of the dialog to show
* @return {Promise} a promise for the user's choice
*/
DialogService.prototype.getUserChoice = function (dialogModel) {
// Show the overlay-options dialog
return this.getDialogResponse(
"overlay-options",
{ dialog: dialogModel }
);
};
return DialogService;
}
);
);

@ -43,57 +43,63 @@ define(
* particularly where a multiple-overlay effect is not specifically
* desired).
*
* @memberof platform/commonUI/dialog
* @constructor
*/
function OverlayService($document, $compile, $rootScope) {
function createOverlay(key, overlayModel) {
// Create a new scope for this overlay
var scope = $rootScope.$new(),
element;
this.$compile = $compile;
// Stop showing the overlay; additionally, release the scope
// that it uses.
function dismiss() {
scope.$destroy();
element.remove();
}
// If no model is supplied, just fill in a default "cancel"
overlayModel = overlayModel || { cancel: dismiss };
// Populate the scope; will be passed directly to the template
scope.overlay = overlayModel;
scope.key = key;
// Create the overlay element and add it to the document's body
element = $compile(TEMPLATE)(scope);
$document.find('body').prepend(element);
return {
dismiss: dismiss
};
}
return {
/**
* Add a new overlay to the document. This will be
* prepended to the document body; the overlay's
* template (as pointed to by the `key` argument) is
* responsible for having a useful z-order, and for
* blocking user interactions if appropriate.
*
* @param {string} key the symbolic key which identifies
* the template of the overlay to be shown
* @param {object} overlayModel the model to pass to the
* included overlay template (this will be passed
* in via ng-model)
*/
createOverlay: createOverlay
// Don't include $document and $rootScope directly;
// avoids https://docs.angularjs.org/error/ng/cpws
this.findBody = function () {
return $document.find('body');
};
this.newScope = function () {
return $rootScope.$new();
};
}
/**
* Add a new overlay to the document. This will be
* prepended to the document body; the overlay's
* template (as pointed to by the `key` argument) is
* responsible for having a useful z-order, and for
* blocking user interactions if appropriate.
*
* @param {string} key the symbolic key which identifies
* the template of the overlay to be shown
* @param {object} overlayModel the model to pass to the
* included overlay template (this will be passed
* in via ng-model)
*/
OverlayService.prototype.createOverlay = function (key, overlayModel) {
// Create a new scope for this overlay
var scope = this.newScope(),
element;
// Stop showing the overlay; additionally, release the scope
// that it uses.
function dismiss() {
scope.$destroy();
element.remove();
}
// If no model is supplied, just fill in a default "cancel"
overlayModel = overlayModel || { cancel: dismiss };
// Populate the scope; will be passed directly to the template
scope.overlay = overlayModel;
scope.key = key;
// Create the overlay element and add it to the document's body
element = this.$compile(TEMPLATE)(scope);
this.findBody().prepend(element);
return {
dismiss: dismiss
};
};
return OverlayService;
}
);
);

@ -29,9 +29,26 @@ define(
* The "Cancel" action; the action triggered by clicking Cancel from
* Edit Mode. Exits the editing user interface and invokes object
* capabilities to persist the changes that have been made.
* @constructor
* @memberof platform/commonUI/edit
* @implements {Action}
*/
function CancelAction($location, urlService, context) {
var domainObject = context.domainObject;
this.domainObject = context.domainObject;
this.$location = $location;
this.urlService = urlService;
}
/**
* Cancel editing.
*
* @returns {Promise} a promise that will be fulfilled when
* cancellation has completed
*/
CancelAction.prototype.perform = function () {
var domainObject = this.domainObject,
$location = this.$location,
urlService = this.urlService;
// Look up the object's "editor.completion" capability;
// this is introduced by EditableDomainObject which is
@ -56,25 +73,15 @@ define(
)));
}
return {
/**
* Cancel editing.
*
* @returns {Promise} a promise that will be fulfilled when
* cancellation has completed
*/
perform: function () {
return doCancel(getEditorCapability())
.then(returnToBrowse);
}
};
}
return doCancel(getEditorCapability())
.then(returnToBrowse);
};
/**
* Check if this action is applicable in a given context.
* This will ensure that a domain object is present in the context,
* and that this domain object is in Edit mode.
* @returns true if applicable
* @returns {boolean} true if applicable
*/
CancelAction.appliesTo = function (context) {
var domainObject = (context || {}).domainObject;
@ -84,4 +91,4 @@ define(
return CancelAction;
}
);
);

@ -42,7 +42,9 @@ define(
* mode (typically triggered by the Edit button.) This will
* show the user interface for editing (by way of a change in
* route)
* @memberof platform/commonUI/edit
* @constructor
* @implements {Action}
*/
function EditAction($location, navigationService, $log, context) {
var domainObject = (context || {}).domainObject;
@ -60,17 +62,19 @@ define(
return NULL_ACTION;
}
return {
/**
* Enter edit mode.
*/
perform: function () {
navigationService.setNavigation(domainObject);
$location.path("/edit");
}
};
this.domainObject = domainObject;
this.$location = $location;
this.navigationService = navigationService;
}
/**
* Enter edit mode.
*/
EditAction.prototype.perform = function () {
this.navigationService.setNavigation(this.domainObject);
this.$location.path("/edit");
};
/**
* Check for applicability; verify that a domain object is present
* for this action to be performed upon.
@ -87,4 +91,4 @@ define(
return EditAction;
}
);
);

@ -29,42 +29,43 @@ define(
/**
* Add one domain object to another's composition.
* @constructor
* @memberof platform/commonUI/edit
* @implements {Action}
*/
function LinkAction(context) {
var domainObject = (context || {}).domainObject,
selectedObject = (context || {}).selectedObject,
selectedId = selectedObject && selectedObject.getId();
this.domainObject = (context || {}).domainObject;
this.selectedObject = (context || {}).selectedObject;
this.selectedId = this.selectedObject && this.selectedObject.getId();
}
LinkAction.prototype.perform = function () {
var self = this;
// Add this domain object's identifier
function addId(model) {
if (Array.isArray(model.composition) &&
model.composition.indexOf(selectedId) < 0) {
model.composition.push(selectedId);
model.composition.indexOf(self.selectedId) < 0) {
model.composition.push(self.selectedId);
}
}
// Persist changes to the domain object
function doPersist() {
var persistence = domainObject.getCapability('persistence');
var persistence =
self.domainObject.getCapability('persistence');
return persistence.persist();
}
// Link these objects
function doLink() {
return domainObject.useCapability("mutation", addId)
return self.domainObject.useCapability("mutation", addId)
.then(doPersist);
}
return {
/**
* Perform this action.
*/
perform: function () {
return selectedId && doLink();
}
};
}
return this.selectedId && doLink();
};
return LinkAction;
}
);
);

@ -32,58 +32,58 @@ define(
'use strict';
/**
* Construct an action which will allow an object's metadata to be
* edited.
* Implements the "Edit Properties" action, which prompts the user
* to modify a domain object's properties.
*
* @param {DialogService} dialogService a service which will show the dialog
* @param {DomainObject} object the object to be edited
* @param {ActionContext} context the context in which this action is performed
* @memberof platform/commonUI/edit
* @implements {Action}
* @constructor
*/
function PropertiesAction(dialogService, context) {
var object = context.domainObject;
this.domainObject = (context || {}).domainObject;
this.dialogService = dialogService;
}
PropertiesAction.prototype.perform = function () {
var type = this.domainObject.getCapability('type'),
domainObject = this.domainObject,
dialogService = this.dialogService;
// Persist modifications to this domain object
function doPersist() {
var persistence = object.getCapability('persistence');
var persistence = domainObject.getCapability('persistence');
return persistence && persistence.persist();
}
// Update the domain object model based on user input
function updateModel(userInput, dialog) {
return object.useCapability('mutation', function (model) {
return domainObject.useCapability('mutation', function (model) {
dialog.updateModel(model, userInput);
});
}
function showDialog(type) {
// Create a dialog object to generate the form structure, etc.
var dialog = new PropertiesDialog(type, object.getModel());
var dialog =
new PropertiesDialog(type, domainObject.getModel());
// Show the dialog
return dialogService.getUserInput(
dialog.getFormStructure(),
dialog.getInitialFormValue()
).then(function (userInput) {
// Update the model, if user input was provided
return userInput && updateModel(userInput, dialog);
}).then(function (result) {
return result && doPersist();
});
// Update the model, if user input was provided
return userInput && updateModel(userInput, dialog);
}).then(function (result) {
return result && doPersist();
});
}
return {
/**
* Perform this action.
* @return {Promise} a promise which will be
* fulfilled when the action has completed.
*/
perform: function () {
var type = object.getCapability('type');
return type && showDialog(type);
}
};
}
return type && showDialog(type);
};
/**
* Filter this action for applicability against a given context.
@ -106,3 +106,4 @@ define(
);

@ -21,12 +21,6 @@
*****************************************************************************/
/*global define*/
/**
* Defines the PropertiesDialog, used by the PropertiesAction to
* populate the form shown in dialog based on the created type.
*
* @module common/actions/properties-dialog
*/
define(
function () {
'use strict';
@ -37,58 +31,60 @@ define(
* @param {TypeImpl} type the type of domain object for which properties
* will be specified
* @param {DomainObject} the object for which properties will be set
* @memberof platform/commonUI/edit
* @constructor
* @memberof module:common/actions/properties-dialog
*/
function PropertiesDialog(type, model) {
var properties = type.getProperties();
return {
/**
* Get sections provided by this dialog.
* @return {FormStructure} the structure of this form
*/
getFormStructure: function () {
return {
name: "Edit " + model.name,
sections: [{
name: "Properties",
rows: properties.map(function (property, index) {
// Property definition is same as form row definition
var row = Object.create(property.getDefinition());
row.key = index;
return row;
})
}]
};
},
/**
* Get the initial state of the form shown by this dialog
* (based on the object model)
* @returns {object} initial state of the form
*/
getInitialFormValue: function () {
// Start with initial values for properties
// Note that index needs to correlate to row.key
// from getFormStructure
return properties.map(function (property) {
return property.getValue(model);
});
},
/**
* Update a domain object model based on the value of a form.
*/
updateModel: function (model, formValue) {
// Update all properties
properties.forEach(function (property, index) {
property.setValue(model, formValue[index]);
});
}
};
this.type = type;
this.model = model;
this.properties = type.getProperties();
}
/**
* Get sections provided by this dialog.
* @return {FormStructure} the structure of this form
*/
PropertiesDialog.prototype.getFormStructure = function () {
return {
name: "Edit " + this.model.name,
sections: [{
name: "Properties",
rows: this.properties.map(function (property, index) {
// Property definition is same as form row definition
var row = Object.create(property.getDefinition());
row.key = index;
return row;
})
}]
};
};
/**
* Get the initial state of the form shown by this dialog
* (based on the object model)
* @returns {object} initial state of the form
*/
PropertiesDialog.prototype.getInitialFormValue = function () {
var model = this.model;
// Start with initial values for properties
// Note that index needs to correlate to row.key
// from getFormStructure
return this.properties.map(function (property) {
return property.getValue(model);
});
};
/**
* Update a domain object model based on the value of a form.
*/
PropertiesDialog.prototype.updateModel = function (model, formValue) {
// Update all properties
this.properties.forEach(function (property, index) {
property.setValue(model, formValue[index]);
});
};
return PropertiesDialog;
}
);
);

@ -37,22 +37,37 @@ define(
*
* @param {DomainObject} object the object to be removed
* @param {ActionContext} context the context in which this action is performed
* @memberof platform/commonUI/edit
* @constructor
* @memberof module:editor/actions/remove-action
* @implements {Action}
*/
function RemoveAction($q, navigationService, context) {
var object = (context || {}).domainObject;
this.domainObject = (context || {}).domainObject;
this.$q = $q;
this.navigationService = navigationService;
}
/**
/**
* Perform this action.
* @return {Promise} a promise which will be
* fulfilled when the action has completed.
*/
RemoveAction.prototype.perform = function () {
var $q = this.$q,
navigationService = this.navigationService,
domainObject = this.domainObject,
ROOT_ID = "ROOT";
/*
* Check whether an object ID matches the ID of the object being
* removed (used to filter a parent's composition to handle the
* removal.)
*/
function isNotObject(otherObjectId) {
return otherObjectId !== object.getId();
return otherObjectId !== domainObject.getId();
}
/**
/*
* Mutate a parent object such that it no longer contains the object
* which is being removed.
*/
@ -60,7 +75,7 @@ define(
model.composition = model.composition.filter(isNotObject);
}
/**
/*
* Invoke persistence on a domain object. This will be called upon
* the removed object's parent (as its composition will have changed.)
*/
@ -68,45 +83,46 @@ define(
var persistence = domainObject.getCapability('persistence');
return persistence && persistence.persist();
}
// Checks current object with object being removed
function checkCurrentObjectNavigation(object, parent) {
var currentObj = navigationService.getNavigation();
if (currentObj.getId() === object.getId()) {
navigationService.setNavigation(parent);
// Checks current object and ascendants of current
// object with object being removed, if the current
// object or any in the current object's path is being removed,
// navigate back to parent of removed object.
function checkObjectNavigation(object, parentObject) {
// Traverse object starts at current location
var traverseObject = (navigationService).getNavigation();
// Stop at ROOT of folder path
while (traverseObject.getId() !== ROOT_ID) {
// If traverse object is object being removed
// navigate to parent of removed object
if (traverseObject.getId() === object.getId()) {
navigationService.setNavigation(parentObject);
return;
}
// Traverses to parent
traverseObject = traverseObject.getCapability('context').getParent();
}
}
/**
/*
* Remove the object from its parent, as identified by its context
* capability.
* @param {object} domain object being removed contextCapability
gotten from the "context" capability of this object
*/
function removeFromContext(object) {
var contextCapability = object.getCapability('context'),
parent = contextCapability.getParent();
// Navigates to parent if deleting current object
checkCurrentObjectNavigation(object, parent);
$q.when(
checkObjectNavigation(object, parent);
return $q.when(
parent.useCapability('mutation', doMutate)
).then(function () {
return doPersist(parent);
});
}
return {
/**
* Perform this action.
* @return {module:core/promises.Promise} a promise which will be
* fulfilled when the action has completed.
*/
perform: function () {
return $q.when(object)
.then(removeFromContext);
}
};
}
return $q.when(domainObject)
.then(removeFromContext);
};
// Object needs to have a parent for Remove to be applicable
RemoveAction.appliesTo = function (context) {
@ -124,4 +140,4 @@ define(
return RemoveAction;
}
);
);

@ -30,9 +30,27 @@ define(
* The "Save" action; the action triggered by clicking Save from
* Edit Mode. Exits the editing user interface and invokes object
* capabilities to persist the changes that have been made.
* @constructor
* @implements {Action}
* @memberof platform/commonUI/edit
*/
function SaveAction($location, urlService, context) {
var domainObject = context.domainObject;
this.domainObject = (context || {}).domainObject;
this.$location = $location;
this.urlService = urlService;
}
/**
* Save changes and conclude editing.
*
* @returns {Promise} a promise that will be fulfilled when
* cancellation has completed
* @memberof platform/commonUI/edit.SaveAction#
*/
SaveAction.prototype.perform = function () {
var domainObject = this.domainObject,
$location = this.$location,
urlService = this.urlService;
// Invoke any save behavior introduced by the editor capability;
// this is introduced by EditableDomainObject which is
@ -51,18 +69,8 @@ define(
));
}
return {
/**
* Save changes and conclude editing.
*
* @returns {Promise} a promise that will be fulfilled when
* cancellation has completed
*/
perform: function () {
return doSave().then(returnToBrowse);
}
};
}
return doSave().then(returnToBrowse);
};
/**
* Check if this action is applicable in a given context.
@ -78,4 +86,4 @@ define(
return SaveAction;
}
);
);

@ -35,6 +35,9 @@ define(
* Meant specifically for use by EditableDomainObject and the
* associated cache; the constructor signature is particular
* to a pattern used there and may contain unused arguments.
* @constructor
* @memberof platform/commonUI/edit
* @implements {CompositionCapability}
*/
return function EditableCompositionCapability(
contextCapability,
@ -54,4 +57,4 @@ define(
);
};
}
);
);

@ -35,6 +35,9 @@ define(
* Meant specifically for use by EditableDomainObject and the
* associated cache; the constructor signature is particular
* to a pattern used there and may contain unused arguments.
* @constructor
* @memberof platform/commonUI/edit
* @implements {ContextCapability}
*/
return function EditableContextCapability(
contextCapability,
@ -72,4 +75,4 @@ define(
return capability;
};
}
);
);

@ -35,6 +35,8 @@ define(
* Meant specifically for use by EditableDomainObject and the
* associated cache; the constructor signature is particular
* to a pattern used there and may contain unused arguments.
* @constructor
* @memberof platform/commonUI/edit
*/
return function EditableLookupCapability(
contextCapability,
@ -76,7 +78,7 @@ define(
// Wrap a returned value (see above); if it's a promise, wrap
// the resolved value.
function wrapResult(result) {
return result.then ? // promise-like
return (result && result.then) ? // promise-like
result.then(makeEditable) :
makeEditable(result);
}
@ -105,8 +107,10 @@ define(
// Wrap a method of this capability
function wrapMethod(fn) {
capability[fn] =
(idempotent ? oneTimeFunction : wrapFunction)(fn);
if (typeof capability[fn] === 'function') {
capability[fn] =
(idempotent ? oneTimeFunction : wrapFunction)(fn);
}
}
// Wrap all methods; return only editable domain objects.
@ -115,4 +119,4 @@ define(
return capability;
};
}
);
);

@ -35,6 +35,9 @@ define(
* Meant specifically for use by EditableDomainObject and the
* associated cache; the constructor signature is particular
* to a pattern used there and may contain unused arguments.
* @constructor
* @memberof platform/commonUI/edit
* @implements {PersistenceCapability}
*/
function EditablePersistenceCapability(
persistenceCapability,
@ -62,4 +65,4 @@ define(
return EditablePersistenceCapability;
}
);
);

@ -35,6 +35,9 @@ define(
* Meant specifically for use by EditableDomainObject and the
* associated cache; the constructor signature is particular
* to a pattern used there and may contain unused arguments.
* @constructor
* @memberof platform/commonUI/edit
* @implements {RelationshipCapability}
*/
return function EditableRelationshipCapability(
relationshipCapability,
@ -54,4 +57,4 @@ define(
);
};
}
);
);

@ -39,27 +39,48 @@ define(
* Meant specifically for use by EditableDomainObject and the
* associated cache; the constructor signature is particular
* to a pattern used there and may contain unused arguments.
* @constructor
* @memberof platform/commonUI/edit
*/
return function EditorCapability(
function EditorCapability(
persistenceCapability,
editableObject,
domainObject,
cache
) {
this.editableObject = editableObject;
this.domainObject = domainObject;
this.cache = cache;
}
// Simulate Promise.resolve (or $q.when); the former
// causes a delayed reaction from Angular (since it
// does not trigger a digest) and the latter is not
// readily accessible, since we're a few classes
// removed from the layer which gets dependency
// injection.
function resolvePromise(value) {
return (value && value.then) ? value : {
then: function (callback) {
return resolvePromise(callback(value));
}
};
}
// Simulate Promise.resolve (or $q.when); the former
// causes a delayed reaction from Angular (since it
// does not trigger a digest) and the latter is not
// readily accessible, since we're a few classes
// removed from the layer which gets dependency
// injection.
function resolvePromise(value) {
return (value && value.then) ? value : {
then: function (callback) {
return resolvePromise(callback(value));
}
};
}
/**
* Save any changes that have been made to this domain object
* (as well as to others that might have been retrieved and
* modified during the editing session)
* @param {boolean} nonrecursive if true, save only this
* object (and not other objects with associated changes)
* @returns {Promise} a promise that will be fulfilled after
* persistence has completed.
* @memberof platform/commonUI/edit.EditorCapability#
*/
EditorCapability.prototype.save = function (nonrecursive) {
var domainObject = this.domainObject,
editableObject = this.editableObject,
cache = this.cache;
// Update the underlying, "real" domain object's model
// with changes made to the copy used for editing.
@ -74,39 +95,32 @@ define(
return domainObject.getCapability('persistence').persist();
}
return {
/**
* Save any changes that have been made to this domain object
* (as well as to others that might have been retrieved and
* modified during the editing session)
* @param {boolean} nonrecursive if true, save only this
* object (and not other objects with associated changes)
* @returns {Promise} a promise that will be fulfilled after
* persistence has completed.
*/
save: function (nonrecursive) {
return nonrecursive ?
resolvePromise(doMutate()).then(doPersist) :
resolvePromise(cache.saveAll());
},
/**
* Cancel editing; Discard any changes that have been made to
* this domain object (as well as to others that might have
* been retrieved and modified during the editing session)
* @returns {Promise} a promise that will be fulfilled after
* cancellation has completed.
*/
cancel: function () {
return resolvePromise(undefined);
},
/**
* Check if there are any unsaved changes.
* @returns {boolean} true if there are unsaved changes
*/
dirty: function () {
return cache.dirty();
}
};
return nonrecursive ?
resolvePromise(doMutate()).then(doPersist) :
resolvePromise(cache.saveAll());
};
/**
* Cancel editing; Discard any changes that have been made to
* this domain object (as well as to others that might have
* been retrieved and modified during the editing session)
* @returns {Promise} a promise that will be fulfilled after
* cancellation has completed.
* @memberof platform/commonUI/edit.EditorCapability#
*/
EditorCapability.prototype.cancel = function () {
return resolvePromise(undefined);
};
/**
* Check if there are any unsaved changes.
* @returns {boolean} true if there are unsaved changes
* @memberof platform/commonUI/edit.EditorCapability#
*/
EditorCapability.prototype.dirty = function () {
return this.cache.dirty();
};
return EditorCapability;
}
);
);

@ -33,6 +33,7 @@ define(
/**
* Controller which supplies action instances for Save/Cancel.
* @memberof platform/commonUI/edit
* @constructor
*/
function EditActionController($scope) {
@ -51,4 +52,4 @@ define(
return EditActionController;
}
);
);

@ -22,7 +22,8 @@
/*global define,Promise*/
/**
* Module defining EditController. Created by vwoeltje on 11/14/14.
* This bundle implements Edit mode.
* @namespace platform/commonUI/edit
*/
define(
["../objects/EditableDomainObject"],
@ -33,15 +34,16 @@ define(
* Controller which is responsible for populating the scope for
* Edit mode; introduces an editable version of the currently
* navigated domain object into the scope.
* @memberof platform/commonUI/edit
* @constructor
*/
function EditController($scope, $q, navigationService) {
var navigatedObject;
var self = this;
function setNavigation(domainObject) {
// Wrap the domain object such that all mutation is
// confined to edit mode (until Save)
navigatedObject =
self.navigatedDomainObject =
domainObject && new EditableDomainObject(domainObject, $q);
}
@ -50,33 +52,33 @@ define(
$scope.$on("$destroy", function () {
navigationService.removeListener(setNavigation);
});
return {
/**
* Get the domain object which is navigated-to.
* @returns {DomainObject} the domain object that is navigated-to
*/
navigatedObject: function () {
return navigatedObject;
},
/**
* 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
*/
getUnloadWarning: function () {
var editorCapability = navigatedObject &&
navigatedObject.getCapability("editor"),
hasChanges = editorCapability && editorCapability.dirty();
return hasChanges ?
"Unsaved changes will be lost if you leave this page." :
undefined;
}
};
}
/**
* Get the domain object which is navigated-to.
* @returns {DomainObject} the domain object that is navigated-to
*/
EditController.prototype.navigatedObject = function () {
return this.navigatedDomainObject;
};
/**
* 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
*/
EditController.prototype.getUnloadWarning = function () {
var navigatedObject = this.navigatedDomainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor"),
hasChanges = editorCapability && editorCapability.dirty();
return hasChanges ?
"Unsaved changes will be lost if you leave this page." :
undefined;
};
return EditController;
}
);
);

@ -28,15 +28,17 @@ define(
/**
* Supports the Library and Elements panes in Edit mode.
* @memberof platform/commonUI/edit
* @constructor
*/
function EditPanesController($scope) {
var root;
var self = this;
// Update root object based on represented object
function updateRoot(domainObject) {
var context = domainObject &&
domainObject.getCapability('context'),
var root = self.rootDomainObject,
context = domainObject &&
domainObject.getCapability('context'),
newRoot = context && context.getTrueRoot(),
oldId = root && root.getId(),
newId = newRoot && newRoot.getId();
@ -44,25 +46,22 @@ define(
// Only update if this has actually changed,
// to avoid excessive refreshing.
if (oldId !== newId) {
root = newRoot;
self.rootDomainObject = newRoot;
}
}
// Update root when represented object changes
$scope.$watch('domainObject', updateRoot);
return {
/**
* Get the root-level domain object, as reported by the
* represented domain object.
* @returns {DomainObject} the root object
*/
getRoot: function () {
return root;
}
};
}
/**
* Get the root-level domain object, as reported by the
* represented domain object.
* @returns {DomainObject} the root object
*/
EditPanesController.prototype.getRoot = function () {
return this.rootDomainObject;
};
return EditPanesController;
}
);
);

@ -31,6 +31,7 @@ define(
* 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
*/
@ -102,4 +103,4 @@ define(
return MCTBeforeUnload;
}
);
);

@ -68,6 +68,9 @@ define(
* which need to behave differently in edit mode,
* and provides a "working copy" of the object's
* model to allow changes to be easily cancelled.
* @constructor
* @memberof platform/commonUI/edit
* @implements {DomainObject}
*/
function EditableDomainObject(domainObject, $q) {
// The cache will hold all domain objects reached from
@ -92,10 +95,10 @@ define(
this,
delegateArguments
),
factory = capabilityFactories[name];
Factory = capabilityFactories[name];
return (factory && capability) ?
factory(capability, editableObject, domainObject, cache) :
return (Factory && capability) ?
new Factory(capability, editableObject, domainObject, cache) :
capability;
};
@ -109,4 +112,4 @@ define(
return EditableDomainObject;
}
);
);

@ -22,7 +22,7 @@
/*global define*/
/**
/*
* An editable domain object cache stores domain objects that have been
* made editable, in a group that can be saved all-at-once. This supports
* Edit mode, which is launched for a specific object but may contain
@ -32,8 +32,6 @@
* to ensure that changes made while in edit mode do not propagate up
* to the objects used in browse mode (or to persistence) until the user
* initiates a Save.
*
* @module editor/object/editable-domain-object-cache
*/
define(
["./EditableModelCache"],
@ -46,107 +44,118 @@ define(
* of objects retrieved via composition or context capabilities as
* editable domain objects.
*
* @param {Constructor<EditableDomainObject>} EditableDomainObject a
* @param {Constructor<DomainObject>} EditableDomainObject a
* constructor function which takes a regular domain object as
* an argument, and returns an editable domain object as its
* result.
* @param $q Angular's $q, for promise handling
* @memberof platform/commonUI/edit
* @constructor
* @memberof module:editor/object/editable-domain-object-cache
*/
function EditableDomainObjectCache(EditableDomainObject, $q) {
var cache = new EditableModelCache(),
dirty = {},
root;
return {
/**
* Wrap this domain object in an editable form, or pull such
* an object from the cache if one already exists.
*
* @param {DomainObject} domainObject the regular domain object
* @returns {DomainObject} the domain object in an editable form
*/
getEditableObject: function (domainObject) {
var type = domainObject.getCapability('type');
// Track the top-level domain object; this will have
// some special behavior for its context capability.
root = root || domainObject;
// Avoid double-wrapping (WTD-1017)
if (domainObject.hasCapability('editor')) {
return domainObject;
}
// Don't bother wrapping non-editable objects
if (!type || !type.hasFeature('creation')) {
return domainObject;
}
// Provide an editable form of the object
return new EditableDomainObject(
domainObject,
cache.getCachedModel(domainObject)
);
},
/**
* Check if a domain object is (effectively) the top-level
* object in this editable subgraph.
* @returns {boolean} true if it is the root
*/
isRoot: function (domainObject) {
return domainObject === root;
},
/**
* Mark an editable domain object (presumably already cached)
* as having received modifications during editing; it should be
* included in the bulk save invoked when editing completes.
*
* @param {DomainObject} domainObject the domain object
*/
markDirty: function (domainObject) {
dirty[domainObject.getId()] = domainObject;
},
/**
* Mark an object (presumably already cached) as having had its
* changes saved (and thus no longer needing to be subject to a
* save operation.)
*
* @param {DomainObject} domainObject the domain object
*/
markClean: function (domainObject) {
delete dirty[domainObject.getId()];
},
/**
* Initiate a save on all objects that have been cached.
*/
saveAll: function () {
// Get a list of all dirty objects
var objects = Object.keys(dirty).map(function (k) {
return dirty[k];
});
// Clear dirty set, since we're about to save.
dirty = {};
// Most save logic is handled by the "editor.completion"
// capability, so that is delegated here.
return $q.all(objects.map(function (object) {
// Save; pass a nonrecursive flag to avoid looping
return object.getCapability('editor').save(true);
}));
},
/**
* Check if any objects have been marked dirty in this cache.
* @returns {boolean} true if objects are dirty
*/
dirty: function () {
return Object.keys(dirty).length > 0;
}
};
this.cache = new EditableModelCache();
this.dirtyObjects = {};
this.root = undefined;
this.$q = $q;
this.EditableDomainObject = EditableDomainObject;
}
/**
* Wrap this domain object in an editable form, or pull such
* an object from the cache if one already exists.
*
* @param {DomainObject} domainObject the regular domain object
* @returns {DomainObject} the domain object in an editable form
*/
EditableDomainObjectCache.prototype.getEditableObject = function (domainObject) {
var type = domainObject.getCapability('type'),
EditableDomainObject = this.EditableDomainObject;
// Track the top-level domain object; this will have
// some special behavior for its context capability.
this.root = this.root || domainObject;
// Avoid double-wrapping (WTD-1017)
if (domainObject.hasCapability('editor')) {
return domainObject;
}
// Don't bother wrapping non-editable objects
if (!type || !type.hasFeature('creation')) {
return domainObject;
}
// Provide an editable form of the object
return new EditableDomainObject(
domainObject,
this.cache.getCachedModel(domainObject)
);
};
/**
* Check if a domain object is (effectively) the top-level
* object in this editable subgraph.
* @returns {boolean} true if it is the root
*/
EditableDomainObjectCache.prototype.isRoot = function (domainObject) {
return domainObject === this.root;
};
/**
* Mark an editable domain object (presumably already cached)
* as having received modifications during editing; it should be
* included in the bulk save invoked when editing completes.
*
* @param {DomainObject} domainObject the domain object
* @memberof platform/commonUI/edit.EditableDomainObjectCache#
*/
EditableDomainObjectCache.prototype.markDirty = function (domainObject) {
this.dirtyObjects[domainObject.getId()] = domainObject;
};
/**
* Mark an object (presumably already cached) as having had its
* changes saved (and thus no longer needing to be subject to a
* save operation.)
*
* @param {DomainObject} domainObject the domain object
*/
EditableDomainObjectCache.prototype.markClean = function (domainObject) {
delete this.dirtyObjects[domainObject.getId()];
};
/**
* Initiate a save on all objects that have been cached.
* @return {Promise} A promise which will resolve when all objects are
* persisted.
*/
EditableDomainObjectCache.prototype.saveAll = function () {
// Get a list of all dirty objects
var dirty = this.dirtyObjects,
objects = Object.keys(dirty).map(function (k) {
return dirty[k];
});
// Clear dirty set, since we're about to save.
this.dirtyObjects = {};
// Most save logic is handled by the "editor.completion"
// capability, so that is delegated here.
return this.$q.all(objects.map(function (object) {
// Save; pass a nonrecursive flag to avoid looping
return object.getCapability('editor').save(true);
}));
};
/**
* Check if any objects have been marked dirty in this cache.
* @returns {boolean} true if objects are dirty
*/
EditableDomainObjectCache.prototype.dirty = function () {
return Object.keys(this.dirtyObjects).length > 0;
};
return EditableDomainObjectCache;
}
);

@ -31,33 +31,32 @@ define(
* made editable, to support a group that can be saved all-at-once.
* This is useful in Edit mode, which is launched for a specific
* object but may contain changes across many objects.
* @memberof platform/commonUI/edit
* @constructor
*/
function EditableModelCache() {
var cache = {};
// Deep-copy a model. Models are JSONifiable, so this can be
// done by stringification then destringification
function clone(model) {
return JSON.parse(JSON.stringify(model));
}
return {
/**
* Get this domain object's model from the cache (or
* place it in the cache if it isn't in the cache yet)
* @returns a clone of the domain object's model
*/
getCachedModel: function (domainObject) {
var id = domainObject.getId();
return (cache[id] =
cache[id] || clone(domainObject.getModel()));
}
};
this.cache = {};
}
// Deep-copy a model. Models are JSONifiable, so this can be
// done by stringification then destringification
function clone(model) {
return JSON.parse(JSON.stringify(model));
}
/**
* Get this domain object's model from the cache (or
* place it in the cache if it isn't in the cache yet)
* @returns a clone of the domain object's model
*/
EditableModelCache.prototype.getCachedModel = function (domainObject) {
var id = domainObject.getId(),
cache = this.cache;
return (cache[id] =
cache[id] || clone(domainObject.getModel()));
};
return EditableModelCache;
}
);
);

@ -30,53 +30,47 @@ define(
* Policy controlling when the `edit` and/or `properties` actions
* can appear as applicable actions of the `view-control` category
* (shown as buttons in the top-right of browse mode.)
* @memberof platform/commonUI/edit
* @constructor
* @implements {Policy.<Action, ActionContext>}
*/
function EditActionPolicy() {
// Get a count of views which are not flagged as non-editable.
function countEditableViews(context) {
var domainObject = (context || {}).domainObject,
views = domainObject && domainObject.useCapability('view'),
count = 0;
}
// A view is editable unless explicitly flagged as not
(views || []).forEach(function (view) {
count += (view.editable !== false) ? 1 : 0;
});
// Get a count of views which are not flagged as non-editable.
function countEditableViews(context) {
var domainObject = (context || {}).domainObject,
views = domainObject && domainObject.useCapability('view'),
count = 0;
return count;
// A view is editable unless explicitly flagged as not
(views || []).forEach(function (view) {
count += (view.editable !== false) ? 1 : 0;
});
return count;
}
EditActionPolicy.prototype.allow = function (action, context) {
var key = action.getMetadata().key,
category = (context || {}).category;
// Only worry about actions in the view-control category
if (category === 'view-control') {
// Restrict 'edit' to cases where there are editable
// views (similarly, restrict 'properties' to when
// the converse is true)
if (key === 'edit') {
return countEditableViews(context) > 0;
} else if (key === 'properties') {
return countEditableViews(context) < 1;
}
}
return {
/**
* Check whether or not a given action is allowed by this
* policy.
* @param {Action} action the action
* @param context the context
* @returns {boolean} true if not disallowed
*/
allow: function (action, context) {
var key = action.getMetadata().key,
category = (context || {}).category;
// Only worry about actions in the view-control category
if (category === 'view-control') {
// Restrict 'edit' to cases where there are editable
// views (similarly, restrict 'properties' to when
// the converse is true)
if (key === 'edit') {
return countEditableViews(context) > 0;
} else if (key === 'properties') {
return countEditableViews(context) < 1;
}
}
// Like all policies, allow by default.
return true;
}
};
}
// Like all policies, allow by default.
return true;
};
return EditActionPolicy;
}
);
);

@ -28,30 +28,24 @@ define(
/**
* Policy controlling which views should be visible in Edit mode.
* @memberof platform/commonUI/edit
* @constructor
* @implements {Policy.<View, DomainObject>}
*/
function EditableViewPolicy() {
return {
/**
* Check whether or not a given action is allowed by this
* policy.
* @param {Action} action the action
* @param domainObject the domain object which will be viewed
* @returns {boolean} true if not disallowed
*/
allow: function (view, domainObject) {
// If a view is flagged as non-editable, only allow it
// while we're not in Edit mode.
if ((view || {}).editable === false) {
return !domainObject.hasCapability('editor');
}
// Like all policies, allow by default.
return true;
}
};
}
EditableViewPolicy.prototype.allow = function (view, domainObject) {
// If a view is flagged as non-editable, only allow it
// while we're not in Edit mode.
if ((view || {}).editable === false) {
return !domainObject.hasCapability('editor');
}
// Like all policies, allow by default.
return true;
};
return EditableViewPolicy;
}
);
);

@ -41,14 +41,17 @@ define(
* and may be reused for different domain objects and/or
* representations resulting from changes there.
*
* @memberof platform/commonUI/edit
* @implements {Representer}
* @constructor
*/
function EditRepresenter($q, $log, scope) {
var domainObject,
key;
var self = this;
// Mutate and persist a new version of a domain object's model.
function doPersist(model) {
var domainObject = self.domainObject;
// First, mutate; then, persist.
return $q.when(domainObject.useCapability("mutation", function () {
return model;
@ -64,7 +67,8 @@ define(
// Look up from scope; these will have been populated by
// mct-representation.
var model = scope.model,
configuration = scope.configuration;
configuration = scope.configuration,
domainObject = self.domainObject;
// Log the commit message
$log.debug([
@ -78,50 +82,33 @@ define(
if (domainObject && domainObject.hasCapability("persistence")) {
// Configurations for specific views are stored by
// key in the "configuration" field of the model.
if (key && configuration) {
if (self.key && configuration) {
model.configuration = model.configuration || {};
model.configuration[key] = configuration;
model.configuration[self.key] = configuration;
}
doPersist(model);
}
}
// Respond to the destruction of the current representation.
function destroy() {
// Nothing to clean up
}
// Handle a specific representation of a specific domain object
function represent(representation, representedObject) {
// Track the key, to know which view configuration to save to.
key = (representation || {}).key;
// Track the represented object
domainObject = representedObject;
// Ensure existing watches are released
destroy();
}
// Place the "commit" method in the scope
scope.commit = commit;
return {
/**
* Set the current representation in use, and the domain
* object being represented.
*
* @param {RepresentationDefinition} representation the
* definition of the representation in use
* @param {DomainObject} domainObject the domain object
* being represented
*/
represent: represent,
/**
* Release any resources associated with this representer.
*/
destroy: destroy
};
}
// Handle a specific representation of a specific domain object
EditRepresenter.prototype.represent = function represent(representation, representedObject) {
// Track the key, to know which view configuration to save to.
this.key = (representation || {}).key;
// Track the represented object
this.domainObject = representedObject;
// Ensure existing watches are released
this.destroy();
};
// Respond to the destruction of the current representation.
EditRepresenter.prototype.destroy = function destroy() {
// Nothing to clean up
};
return EditRepresenter;
}
);
);

@ -38,125 +38,23 @@ define(
*
* @param structure toolbar structure, as provided by view definition
* @param {Function} commit callback to invoke after changes
* @memberof platform/commonUI/edit
* @constructor
*/
function EditToolbar(structure, commit) {
var toolbarStructure = Object.create(structure || {}),
toolbarState,
selection,
properties = [];
var self = this;
// Generate a new key for an item's property
function addKey(property) {
properties.push(property);
return properties.length - 1; // Return index of property
}
// Update value for this property in all elements of the
// selection which have this property.
function updateProperties(property, value) {
var changed = false;
// Update property in a selected element
function updateProperty(selected) {
// Ignore selected elements which don't have this property
if (selected[property] !== undefined) {
// Check if this is a setter, or just assignable
if (typeof selected[property] === 'function') {
changed =
changed || (selected[property]() !== value);
selected[property](value);
} else {
changed =
changed || (selected[property] !== value);
selected[property] = value;
}
}
}
// Update property in all selected elements
selection.forEach(updateProperty);
// Return whether or not anything changed
return changed;
}
// Look up the current value associated with a property
// in selection i
function lookupState(property, selected) {
var value = selected[property];
return (typeof value === 'function') ? value() : value;
}
// Get initial value for a given property
function initializeState(property) {
var result;
// Look through all selections for this property;
// values should all match by the time we perform
// this lookup anyway.
selection.forEach(function (selected) {
result = (selected[property] !== undefined) ?
lookupState(property, selected) :
result;
});
return result;
}
// Check if all elements of the selection which have this
// property have the same value for this property.
function isConsistent(property) {
var consistent = true,
observed = false,
state;
// Check if a given element of the selection is consistent
// with previously-observed elements for this property.
function checkConsistency(selected) {
var next;
// Ignore selections which don't have this property
if (selected[property] !== undefined) {
// Look up state of this element in the selection
next = lookupState(property, selected);
// Detect inconsistency
if (observed) {
consistent = consistent && (next === state);
}
// Track state for next iteration
state = next;
observed = true;
}
}
// Iterate through selections
selection.forEach(checkConsistency);
return consistent;
}
// Used to filter out items which are applicable (or not)
// to the current selection.
function isApplicable(item) {
var property = (item || {}).property,
method = (item || {}).method,
exclusive = !!(item || {}).exclusive;
// Check if a selected item defines this property
function hasProperty(selected) {
return (property && (selected[property] !== undefined)) ||
(method && (typeof selected[method] === 'function'));
}
return selection.map(hasProperty).reduce(
exclusive ? and : or,
exclusive
) && isConsistent(property);
self.properties.push(property);
return self.properties.length - 1; // Return index of property
}
// Invoke all functions in selections with the given name
function invoke(method, value) {
if (method) {
// Make the change in the selection
selection.forEach(function (selected) {
self.selection.forEach(function (selected) {
if (typeof selected[method] === 'function') {
selected[method](value);
}
@ -189,73 +87,172 @@ define(
return converted;
}
this.toolbarState = [];
this.selection = undefined;
this.properties = [];
this.toolbarStructure = Object.create(structure || {});
this.toolbarStructure.sections =
((structure || {}).sections || []).map(convertSection);
}
// Check if all elements of the selection which have this
// property have the same value for this property.
EditToolbar.prototype.isConsistent = function (property) {
var self = this,
consistent = true,
observed = false,
state;
// Check if a given element of the selection is consistent
// with previously-observed elements for this property.
function checkConsistency(selected) {
var next;
// Ignore selections which don't have this property
if (selected[property] !== undefined) {
// Look up state of this element in the selection
next = self.lookupState(property, selected);
// Detect inconsistency
if (observed) {
consistent = consistent && (next === state);
}
// Track state for next iteration
state = next;
observed = true;
}
}
// Iterate through selections
self.selection.forEach(checkConsistency);
return consistent;
};
// Used to filter out items which are applicable (or not)
// to the current selection.
EditToolbar.prototype.isApplicable = function (item) {
var property = (item || {}).property,
method = (item || {}).method,
exclusive = !!(item || {}).exclusive;
// Check if a selected item defines this property
function hasProperty(selected) {
return (property && (selected[property] !== undefined)) ||
(method && (typeof selected[method] === 'function'));
}
return this.selection.map(hasProperty).reduce(
exclusive ? and : or,
exclusive
) && this.isConsistent(property);
};
// Look up the current value associated with a property
EditToolbar.prototype.lookupState = function (property, selected) {
var value = selected[property];
return (typeof value === 'function') ? value() : value;
};
/**
* Set the current selection. Visibility of sections
* and items in the toolbar will be updated to match this.
* @param {Array} s the new selection
*/
EditToolbar.prototype.setSelection = function (s) {
var self = this;
// Show/hide controls in this section per applicability
function refreshSectionApplicability(section) {
var count = 0;
// Show/hide each item
(section.items || []).forEach(function (item) {
item.hidden = !isApplicable(item);
item.hidden = !self.isApplicable(item);
count += item.hidden ? 0 : 1;
});
// Hide this section if there are no applicable items
section.hidden = !count;
}
// Show/hide controls if they are applicable
function refreshApplicability() {
toolbarStructure.sections.forEach(refreshSectionApplicability);
// Get initial value for a given property
function initializeState(property) {
var result;
// Look through all selections for this property;
// values should all match by the time we perform
// this lookup anyway.
self.selection.forEach(function (selected) {
result = (selected[property] !== undefined) ?
self.lookupState(property, selected) :
result;
});
return result;
}
// Refresh toolbar state to match selection
function refreshState() {
toolbarState = properties.map(initializeState);
}
this.selection = s;
this.toolbarStructure.sections.forEach(refreshSectionApplicability);
this.toolbarState = this.properties.map(initializeState);
};
toolbarStructure.sections =
((structure || {}).sections || []).map(convertSection);
/**
* Get the structure of the toolbar, as appropriate to
* pass to `mct-toolbar`.
* @returns the toolbar structure
*/
EditToolbar.prototype.getStructure = function () {
return this.toolbarStructure;
};
toolbarState = [];
/**
* Get the current state of the toolbar, as appropriate
* to two-way bind to the state handled by `mct-toolbar`.
* @returns {Array} state of the toolbar
*/
EditToolbar.prototype.getState = function () {
return this.toolbarState;
};
return {
/**
* Set the current selection. Visisbility of sections
* and items in the toolbar will be updated to match this.
* @param {Array} s the new selection
*/
setSelection: function (s) {
selection = s;
refreshApplicability();
refreshState();
},
/**
* Get the structure of the toolbar, as appropriate to
* pass to `mct-toolbar`.
* @returns the toolbar structure
*/
getStructure: function () {
return toolbarStructure;
},
/**
* Get the current state of the toolbar, as appropriate
* to two-way bind to the state handled by `mct-toolbar`.
* @returns {Array} state of the toolbar
*/
getState: function () {
return toolbarState;
},
/**
* Update state within the current selection.
* @param {number} index the index of the corresponding
* element in the state array
* @param value the new value to convey to the selection
*/
updateState: function (index, value) {
return updateProperties(properties[index], value);
/**
* Update state within the current selection.
* @param {number} index the index of the corresponding
* element in the state array
* @param value the new value to convey to the selection
*/
EditToolbar.prototype.updateState = function (index, value) {
var self = this;
// Update value for this property in all elements of the
// selection which have this property.
function updateProperties(property, value) {
var changed = false;
// Update property in a selected element
function updateProperty(selected) {
// Ignore selected elements which don't have this property
if (selected[property] !== undefined) {
// Check if this is a setter, or just assignable
if (typeof selected[property] === 'function') {
changed =
changed || (selected[property]() !== value);
selected[property](value);
} else {
changed =
changed || (selected[property] !== value);
selected[property] = value;
}
}
}
};
}
// Update property in all selected elements
self.selection.forEach(updateProperty);
// Return whether or not anything changed
return changed;
}
return updateProperties(this.properties[index], value);
};
return EditToolbar;
}
);

@ -27,17 +27,21 @@ define(
"use strict";
// No operation
function noop() {}
var NOOP_REPRESENTER = {
represent: function () {},
destroy: function () {}
};
/**
* The EditToolbarRepresenter populates the toolbar in Edit mode
* based on a view's definition.
* @param {Scope} scope the Angular scope of the representation
* @memberof platform/commonUI/edit
* @constructor
* @implements {Representer}
*/
function EditToolbarRepresenter(scope, element, attrs) {
var toolbar,
toolbarObject = {};
var self = this;
// Mark changes as ready to persist
function commit(message) {
@ -49,31 +53,33 @@ define(
// Handle changes to the current selection
function updateSelection(selection) {
// Only update if there is a toolbar to update
if (toolbar) {
if (self.toolbar) {
// Make sure selection is array-like
selection = Array.isArray(selection) ?
selection :
(selection ? [selection] : []);
// Update the toolbar's selection
toolbar.setSelection(selection);
self.toolbar.setSelection(selection);
// ...and expose its structure/state
toolbarObject.structure = toolbar.getStructure();
toolbarObject.state = toolbar.getState();
self.toolbarObject.structure =
self.toolbar.getStructure();
self.toolbarObject.state =
self.toolbar.getState();
}
}
// Get state (to watch it)
function getState() {
return toolbarObject.state;
return self.toolbarObject.state;
}
// Update selection models to match changed toolbar state
function updateState(state) {
// Update underlying state based on toolbar changes
var changed = (state || []).map(function (value, index) {
return toolbar.updateState(index, value);
return self.toolbar.updateState(index, value);
}).reduce(function (a, b) {
return a || b;
}, false);
@ -85,66 +91,73 @@ define(
}
}
// Initialize toolbar (expose object to parent scope)
function initialize(definition) {
// If we have been asked to expose toolbar state...
if (attrs.toolbar) {
// Initialize toolbar object
toolbar = new EditToolbar(definition, commit);
// Ensure toolbar state is exposed
scope.$parent[attrs.toolbar] = toolbarObject;
}
}
// Represent a domain object using this definition
function represent(representation) {
// Get the newest toolbar definition from the view
var definition = (representation || {}).toolbar || {};
// Expose the toolbar object to the parent scope
initialize(definition);
// Create a selection scope
scope.selection = new EditToolbarSelection();
// Initialize toolbar to an empty selection
updateSelection([]);
}
// Destroy; remove toolbar object from parent scope
function destroy() {
// Avoid attaching scope to this;
// http://errors.angularjs.org/1.2.26/ng/cpws
this.setSelection = function (s) {
scope.selection = s;
};
this.clearExposedToolbar = function () {
// Clear exposed toolbar state (if any)
if (attrs.toolbar) {
delete scope.$parent[attrs.toolbar];
}
}
};
this.exposeToolbar = function () {
scope.$parent[self.attrs.toolbar] = self.toolbarObject;
};
this.commit = commit;
this.attrs = attrs;
this.updateSelection = updateSelection;
this.toolbar = undefined;
this.toolbarObject = {};
// If this representation exposes a toolbar, set up watches
// to synchronize with it.
if (attrs.toolbar) {
if (attrs && attrs.toolbar) {
// Detect and handle changes to state from the toolbar
scope.$watchCollection(getState, updateState);
// Watch for changes in the current selection state
scope.$watchCollection("selection.all()", updateSelection);
// Expose toolbar state under that name
scope.$parent[attrs.toolbar] = toolbarObject;
scope.$parent[attrs.toolbar] = this.toolbarObject;
} else {
// No toolbar declared, so do nothing.
return NOOP_REPRESENTER;
}
return {
/**
* Set the current representation in use, and the domain
* object being represented.
*
* @param {RepresentationDefinition} representation the
* definition of the representation in use
* @param {DomainObject} domainObject the domain object
* being represented
*/
represent: (attrs || {}).toolbar ? represent : noop,
/**
* Release any resources associated with this representer.
*/
destroy: (attrs || {}).toolbar ? destroy : noop
};
}
// Represent a domain object using this definition
EditToolbarRepresenter.prototype.represent = function (representation) {
// Get the newest toolbar definition from the view
var definition = (representation || {}).toolbar || {},
self = this;
// Initialize toolbar (expose object to parent scope)
function initialize(definition) {
// If we have been asked to expose toolbar state...
if (self.attrs.toolbar) {
// Initialize toolbar object
self.toolbar = new EditToolbar(definition, self.commit);
// Ensure toolbar state is exposed
self.exposeToolbar();
}
}
// Expose the toolbar object to the parent scope
initialize(definition);
// Create a selection scope
this.setSelection(new EditToolbarSelection());
// Initialize toolbar to an empty selection
this.updateSelection([]);
};
// Destroy; remove toolbar object from parent scope
EditToolbarRepresenter.prototype.destroy = function () {
this.clearExposedToolbar();
};
return EditToolbarRepresenter;
}
);
);

@ -37,110 +37,96 @@ define(
* * The selection, for single selected elements within the
* view.
*
* @memberof platform/commonUI/edit
* @constructor
*/
function EditToolbarSelection() {
var selection = [ {} ],
selecting = false,
selected;
this.selection = [{}];
this.selecting = false;
this.selectedObj = undefined;
}
// Remove the currently-selected object
function deselect() {
// Nothing to do if we don't have a selected object
if (selecting) {
// Clear state tracking
selecting = false;
selected = undefined;
/**
* Check if an object is currently selected.
* @param {*} obj the object to check for selection
* @returns {boolean} true if selected, otherwise false
*/
EditToolbarSelection.prototype.selected = function (obj) {
return (obj === this.selectedObj) || (obj === this.selection[0]);
};
// Remove the selection
selection.pop();
return true;
}
/**
* Select an object.
* @param obj the object to select
* @returns {boolean} true if selection changed
*/
EditToolbarSelection.prototype.select = function (obj) {
// Proxy is always selected
if (obj === this.selection[0]) {
return false;
}
// Select an object
function select(obj) {
// Proxy is always selected
if (obj === selection[0]) {
return false;
}
// Clear any existing selection
this.deselect();
// Clear any existing selection
deselect();
// Note the current selection state
this.selectedObj = obj;
this.selecting = true;
// Note the current selection state
selected = obj;
selecting = true;
// Add the selection
this.selection.push(obj);
};
// Add the selection
selection.push(obj);
/**
* Clear the current selection.
* @returns {boolean} true if selection changed
*/
EditToolbarSelection.prototype.deselect = function () {
// Nothing to do if we don't have a selected object
if (this.selecting) {
// Clear state tracking
this.selecting = false;
this.selectedObj = undefined;
// Remove the selection
this.selection.pop();
return true;
}
return false;
};
/**
* Get the currently-selected object.
* @returns the currently selected object
*/
EditToolbarSelection.prototype.get = function () {
return this.selectedObj;
};
// Check if an object is selected
function isSelected(obj) {
return (obj === selected) || (obj === selection[0]);
/**
* Get/set the view proxy (for toolbar actions taken upon
* the view itself.)
* @param [proxy] the view proxy (if setting)
* @returns the current view proxy
*/
EditToolbarSelection.prototype.proxy = function (p) {
if (arguments.length > 0) {
this.selection[0] = p;
}
return this.selection[0];
};
// Getter for current selection
function get() {
return selected;
}
// Getter/setter for view proxy
function proxy(p) {
if (arguments.length > 0) {
selection[0] = p;
}
return selection[0];
}
// Getter for the full array of selected objects (incl. view proxy)
function all() {
return selection;
}
return {
/**
* Check if an object is currently selected.
* @returns true if selected, otherwise false
*/
selected: isSelected,
/**
* Select an object.
* @param obj the object to select
* @returns {boolean} true if selection changed
*/
select: select,
/**
* Clear the current selection.
* @returns {boolean} true if selection changed
*/
deselect: deselect,
/**
* Get the currently-selected object.
* @returns the currently selected object
*/
get: get,
/**
* Get/set the view proxy (for toolbar actions taken upon
* the view itself.)
* @param [proxy] the view proxy (if setting)
* @returns the current view proxy
*/
proxy: proxy,
/**
* Get an array containing all selections, including the
* selection proxy. It is generally not advisable to
* mutate this array directly.
* @returns {Array} all selections
*/
all: all
};
}
/**
* Get an array containing all selections, including the
* selection proxy. It is generally not advisable to
* mutate this array directly.
* @returns {Array} all selections
*/
EditToolbarSelection.prototype.all = function () {
return this.selection;
};
return EditToolbarSelection;
}
);
);

@ -31,7 +31,11 @@ define(
mockNavigationService,
mockDomainObject,
mockParent,
mockChildObject,
mockGrandchildObject,
mockContext,
mockChildContext,
mockGrandchildContext,
mockMutation,
mockPersistence,
mockType,
@ -55,6 +59,14 @@ define(
"domainObject",
[ "getId", "getCapability" ]
);
mockChildObject = jasmine.createSpyObj(
"domainObject",
[ "getId", "getCapability" ]
);
mockGrandchildObject = jasmine.createSpyObj(
"domainObject",
[ "getId", "getCapability" ]
);
mockQ = { when: mockPromise };
mockParent = {
getModel: function () {
@ -65,12 +77,11 @@ define(
},
useCapability: function (k, v) {
return capabilities[k].invoke(v);
},
getId: function () {
return "test";
}
};
mockContext = jasmine.createSpyObj("context", [ "getParent" ]);
mockChildContext = jasmine.createSpyObj("context", [ "getParent" ]);
mockGrandchildContext = jasmine.createSpyObj("context", [ "getParent" ]);
mockMutation = jasmine.createSpyObj("mutation", [ "invoke" ]);
mockPersistence = jasmine.createSpyObj("persistence", [ "persist" ]);
mockType = jasmine.createSpyObj("type", [ "hasFeature" ]);
@ -97,7 +108,7 @@ define(
type: mockType
};
model = {
composition: [ "a", "test", "b", "c" ]
composition: [ "a", "test", "b" ]
};
actionContext = { domainObject: mockDomainObject };
@ -137,11 +148,66 @@ define(
// Should have removed "test" - that was our
// mock domain object's id.
expect(result.composition).toEqual(["a", "b", "c"]);
expect(result.composition).toEqual(["a", "b"]);
// Finally, should have persisted
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("removes parent of object currently navigated to", function () {
var mutator, result;
// Navigates to child object
mockNavigationService.getNavigation.andReturn(mockChildObject);
// Test is id of object being removed
// Child object has different id
mockDomainObject.getId.andReturn("test");
mockChildObject.getId.andReturn("not test");
// Sets context for the child and domainObject
mockDomainObject.getCapability.andReturn(mockContext);
mockChildObject.getCapability.andReturn(mockChildContext);
// Parents of child and domainObject are set
mockContext.getParent.andReturn(mockParent);
mockChildContext.getParent.andReturn(mockDomainObject);
mockType.hasFeature.andReturn(true);
action.perform();
// Expects navigation to parent of domainObject (removed object)
expect(mockNavigationService.setNavigation).toHaveBeenCalledWith(mockParent);
});
it("checks if removing object not in ascendent path (reaches ROOT)", function () {
// Navigates to grandchild of ROOT
mockNavigationService.getNavigation.andReturn(mockGrandchildObject);
// domainObject (grandparent) is set as ROOT, child and grandchild
// are set objects not being removed
mockDomainObject.getId.andReturn("ROOT");
mockChildObject.getId.andReturn("not test");
mockGrandchildObject.getId.andReturn("not test");
// Sets context for the grandchild, child, and domainObject
mockDomainObject.getCapability.andReturn(mockContext);
mockChildObject.getCapability.andReturn(mockChildContext);
mockGrandchildObject.getCapability.andReturn(mockGrandchildContext);
// Parents of grandchild, child, and domainObject are set
mockContext.getParent.andReturn(mockParent);
mockChildContext.getParent.andReturn(mockDomainObject);
mockGrandchildContext.getParent.andReturn(mockChildObject);
mockType.hasFeature.andReturn(true);
action.perform();
// Expects no navigation to occur
expect(mockNavigationService.setNavigation).not.toHaveBeenCalled();
});
});
}

@ -112,7 +112,9 @@ define(
});
it("saves objects that have been marked dirty", function () {
var objects = ['a', 'b', 'c'].map(TestObject).map(cache.getEditableObject);
var objects = ['a', 'b', 'c'].map(TestObject).map(function (domainObject) {
return cache.getEditableObject(domainObject);
});
cache.markDirty(objects[0]);
cache.markDirty(objects[2]);
@ -123,7 +125,9 @@ define(
});
it("does not save objects that have been marked clean", function () {
var objects = ['a', 'b', 'c'].map(TestObject).map(cache.getEditableObject);
var objects = ['a', 'b', 'c'].map(TestObject).map(function (domainObject) {
return cache.getEditableObject(domainObject);
});
cache.markDirty(objects[0]);
cache.markDirty(objects[2]);

@ -11,8 +11,7 @@
},
{
"key": "agentService",
"implementation": "/services/AgentService.js",
"depends": [ "$window" ]
"implementation": "/services/AgentService.js"
}
],
"runs": [

@ -99,6 +99,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* CONSTANTS */
/*****************************************************************************
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@ -694,44 +695,46 @@ mct-container {
position: absolute;
bottom: 5px; }
/* line 241, ../sass/user-environ/_layout.scss */
.pane.treeview.left .search-holder {
top: 34px; }
/* line 244, ../sass/user-environ/_layout.scss */
.pane.treeview.left .tree-holder {
overflow: auto;
top: 34px;
padding-right: 5px; }
/* line 249, ../sass/user-environ/_layout.scss */
top: 64px; }
/* line 251, ../sass/user-environ/_layout.scss */
.pane.items .object-browse-bar .left.abs, .pane.items .object-browse-bar .btn-menu span.left.l-click-area, .btn-menu .pane.items .object-browse-bar span.left.l-click-area,
.pane.items .object-browse-bar .right.abs,
.pane.items .object-browse-bar .btn-menu span.right.l-click-area,
.btn-menu .pane.items .object-browse-bar span.right.l-click-area {
top: auto; }
/* line 260, ../sass/user-environ/_layout.scss */
/* line 262, ../sass/user-environ/_layout.scss */
.pane.items .object-holder {
top: 34px; }
/* line 264, ../sass/user-environ/_layout.scss */
/* line 266, ../sass/user-environ/_layout.scss */
.pane .object-holder {
overflow: auto; }
/* line 272, ../sass/user-environ/_layout.scss */
/* line 274, ../sass/user-environ/_layout.scss */
.split-layout.horizontal > .pane {
margin-top: 5px; }
/* line 275, ../sass/user-environ/_layout.scss */
/* line 277, ../sass/user-environ/_layout.scss */
.split-layout.horizontal > .pane:first-child {
margin-top: 0; }
/* line 282, ../sass/user-environ/_layout.scss */
/* line 284, ../sass/user-environ/_layout.scss */
.split-layout.vertical > .pane {
margin-left: 5px; }
/* line 285, ../sass/user-environ/_layout.scss */
/* line 287, ../sass/user-environ/_layout.scss */
.split-layout.vertical > .pane > .holder {
left: 0;
right: 0; }
/* line 289, ../sass/user-environ/_layout.scss */
/* line 291, ../sass/user-environ/_layout.scss */
.split-layout.vertical > .pane:first-child {
margin-left: 0; }
/* line 291, ../sass/user-environ/_layout.scss */
/* line 293, ../sass/user-environ/_layout.scss */
.split-layout.vertical > .pane:first-child .holder {
right: 3px; }
/* line 300, ../sass/user-environ/_layout.scss */
/* line 302, ../sass/user-environ/_layout.scss */
.object-browse-bar .btn,
.object-browse-bar .t-btn,
.object-browse-bar .view-switcher,
@ -745,12 +748,12 @@ mct-container {
line-height: 200%;
vertical-align: top; }
/* line 317, ../sass/user-environ/_layout.scss */
/* line 319, ../sass/user-environ/_layout.scss */
.object-browse-bar .view-switcher,
.top-bar .view-switcher {
margin-right: 20px; }
/* line 322, ../sass/user-environ/_layout.scss */
/* line 324, ../sass/user-environ/_layout.scss */
.vscroll {
overflow-y: auto; }
@ -1505,29 +1508,32 @@ table {
table .tr .th:first-child {
border-left: none; }
/* line 85, ../sass/lists/_tabular.scss */
.tabular tr th.sort .icon-sorting:before, .tabular tr .th.sort .icon-sorting:before, .tabular .tr th.sort .icon-sorting:before, .tabular .tr .th.sort .icon-sorting:before,
table tr th.sort .icon-sorting:before,
table tr .th.sort .icon-sorting:before,
table .tr th.sort .icon-sorting:before,
table .tr .th.sort .icon-sorting:before {
display: inline-block;
.tabular tr th.sort.sort:after, .tabular tr .th.sort.sort:after, .tabular .tr th.sort.sort:after, .tabular .tr .th.sort.sort:after,
table tr th.sort.sort:after,
table tr .th.sort.sort:after,
table .tr th.sort.sort:after,
table .tr .th.sort.sort:after {
color: #49dedb;
font-family: symbolsfont;
margin-left: 5px; }
/* line 90, ../sass/lists/_tabular.scss */
.tabular tr th.sort.asc .icon-sorting:before, .tabular tr .th.sort.asc .icon-sorting:before, .tabular .tr th.sort.asc .icon-sorting:before, .tabular .tr .th.sort.asc .icon-sorting:before,
table tr th.sort.asc .icon-sorting:before,
table tr .th.sort.asc .icon-sorting:before,
table .tr th.sort.asc .icon-sorting:before,
table .tr .th.sort.asc .icon-sorting:before {
content: '0'; }
font-size: 8px;
content: "\ed";
display: inline-block;
margin-left: 3px; }
/* line 93, ../sass/lists/_tabular.scss */
.tabular tr th.sort.desc .icon-sorting:before, .tabular tr .th.sort.desc .icon-sorting:before, .tabular .tr th.sort.desc .icon-sorting:before, .tabular .tr .th.sort.desc .icon-sorting:before,
table tr th.sort.desc .icon-sorting:before,
table tr .th.sort.desc .icon-sorting:before,
table .tr th.sort.desc .icon-sorting:before,
table .tr .th.sort.desc .icon-sorting:before {
content: '1'; }
/* line 98, ../sass/lists/_tabular.scss */
.tabular tr th.sort.sort.desc:after, .tabular tr .th.sort.sort.desc:after, .tabular .tr th.sort.sort.desc:after, .tabular .tr .th.sort.sort.desc:after,
table tr th.sort.sort.desc:after,
table tr .th.sort.sort.desc:after,
table .tr th.sort.sort.desc:after,
table .tr .th.sort.sort.desc:after {
content: "\ec"; }
/* line 97, ../sass/lists/_tabular.scss */
.tabular tr th.sortable, .tabular tr .th.sortable, .tabular .tr th.sortable, .tabular .tr .th.sortable,
table tr th.sortable,
table tr .th.sortable,
table .tr th.sortable,
table .tr .th.sortable {
cursor: pointer; }
/* line 101, ../sass/lists/_tabular.scss */
.tabular tr td, .tabular tr .td, .tabular .tr td, .tabular .tr .td,
table tr td,
table tr .td,
@ -1539,21 +1545,21 @@ table {
padding: 3px 5px;
word-wrap: break-word;
vertical-align: top; }
/* line 105, ../sass/lists/_tabular.scss */
/* line 108, ../sass/lists/_tabular.scss */
.tabular tr td.numeric, .tabular tr .td.numeric, .tabular .tr td.numeric, .tabular .tr .td.numeric,
table tr td.numeric,
table tr .td.numeric,
table .tr td.numeric,
table .tr .td.numeric {
text-align: right; }
/* line 108, ../sass/lists/_tabular.scss */
/* line 111, ../sass/lists/_tabular.scss */
.tabular tr td.s-cell-type-value, .tabular tr .td.s-cell-type-value, .tabular .tr td.s-cell-type-value, .tabular .tr .td.s-cell-type-value,
table tr td.s-cell-type-value,
table tr .td.s-cell-type-value,
table .tr td.s-cell-type-value,
table .tr .td.s-cell-type-value {
text-align: right; }
/* line 110, ../sass/lists/_tabular.scss */
/* line 113, ../sass/lists/_tabular.scss */
.tabular tr td.s-cell-type-value .l-cell-contents, .tabular tr .td.s-cell-type-value .l-cell-contents, .tabular .tr td.s-cell-type-value .l-cell-contents, .tabular .tr .td.s-cell-type-value .l-cell-contents,
table tr td.s-cell-type-value .l-cell-contents,
table tr .td.s-cell-type-value .l-cell-contents,
@ -1564,23 +1570,23 @@ table {
border-radius: 2px;
padding-left: 5px;
padding-right: 5px; }
/* line 126, ../sass/lists/_tabular.scss */
/* line 129, ../sass/lists/_tabular.scss */
.tabular.filterable tbody, .tabular.filterable .tbody,
table.filterable tbody,
table.filterable .tbody {
top: 44px; }
/* line 129, ../sass/lists/_tabular.scss */
/* line 132, ../sass/lists/_tabular.scss */
.tabular.filterable input[type="text"],
table.filterable input[type="text"] {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%; }
/* line 135, ../sass/lists/_tabular.scss */
/* line 138, ../sass/lists/_tabular.scss */
.tabular.fixed-header,
table.fixed-header {
height: 100%; }
/* line 137, ../sass/lists/_tabular.scss */
/* line 140, ../sass/lists/_tabular.scss */
.tabular.fixed-header thead, .tabular.fixed-header .thead,
.tabular.fixed-header tbody tr, .tabular.fixed-header .tbody .tr,
table.fixed-header thead,
@ -1589,12 +1595,12 @@ table {
table.fixed-header .tbody .tr {
display: table;
table-layout: fixed; }
/* line 142, ../sass/lists/_tabular.scss */
/* line 145, ../sass/lists/_tabular.scss */
.tabular.fixed-header thead, .tabular.fixed-header .thead,
table.fixed-header thead,
table.fixed-header .thead {
width: calc(100% - 10px); }
/* line 144, ../sass/lists/_tabular.scss */
/* line 147, ../sass/lists/_tabular.scss */
.tabular.fixed-header thead:before, .tabular.fixed-header .thead:before,
table.fixed-header thead:before,
table.fixed-header .thead:before {
@ -1605,7 +1611,7 @@ table {
width: 100%;
height: 22px;
background: rgba(255, 255, 255, 0.15); }
/* line 154, ../sass/lists/_tabular.scss */
/* line 157, ../sass/lists/_tabular.scss */
.tabular.fixed-header tbody, .tabular.fixed-header .tbody,
table.fixed-header tbody,
table.fixed-header .tbody {
@ -1620,7 +1626,7 @@ table {
top: 22px;
display: block;
overflow-y: scroll; }
/* line 162, ../sass/lists/_tabular.scss */
/* line 165, ../sass/lists/_tabular.scss */
.tabular.t-event-messages td, .tabular.t-event-messages .td,
table.t-event-messages td,
table.t-event-messages .td {
@ -2847,22 +2853,23 @@ label.checkbox.custom {
color: #d9d9d9;
line-height: 1.5rem;
padding: 3px 10px 3px 30px;
position: relative;
white-space: nowrap; }
/* line 46, ../sass/controls/_menus.scss */
/* line 47, ../sass/controls/_menus.scss */
.menu-element .menu ul li:first-child {
border: none; }
/* line 49, ../sass/controls/_menus.scss */
/* line 50, ../sass/controls/_menus.scss */
.menu-element .menu ul li:hover {
background: #737373;
color: #fff; }
/* line 55, ../sass/controls/_menus.scss */
/* line 56, ../sass/controls/_menus.scss */
.menu-element .menu ul li:hover .icon {
color: #33ccff; }
/* line 63, ../sass/controls/_menus.scss */
/* line 64, ../sass/controls/_menus.scss */
.menu-element .menu ul li .type-icon {
left: 10px; }
/* line 70, ../sass/controls/_menus.scss */
.menu-element .context-menu,
/* line 71, ../sass/controls/_menus.scss */
.menu-element .context-menu, .menu-element .checkbox-menu,
.menu-element .super-menu {
pointer-events: auto;
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzhjOGM4YyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzgwODA4MCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
@ -2885,35 +2892,56 @@ label.checkbox.custom {
color: #999;
display: inline-block; }
/* line 175, ../sass/_mixins.scss */
.menu-element .context-menu.btn-menu .invoke-menu,
.menu-element .context-menu.btn-menu .invoke-menu, .menu-element .btn-menu.checkbox-menu .invoke-menu,
.menu-element .super-menu.btn-menu .invoke-menu {
color: #b0b0b0; }
/* line 78, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li,
/* line 79, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li, .menu-element .checkbox-menu ul li,
.menu-element .super-menu ul li {
padding-left: 25px; }
/* line 80, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li a,
/* line 81, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li a, .menu-element .checkbox-menu ul li a,
.menu-element .super-menu ul li a {
color: white; }
/* line 81, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li .icon,
/* line 84, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li .icon, .menu-element .checkbox-menu ul li .icon,
.menu-element .super-menu ul li .icon {
color: #24c8ff; }
/* line 84, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li .type-icon,
/* line 87, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li .type-icon, .menu-element .checkbox-menu ul li .type-icon,
.menu-element .super-menu ul li .type-icon {
left: 5px; }
/* line 87, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li:hover .icon,
/* line 90, ../sass/controls/_menus.scss */
.menu-element .context-menu ul li:hover .icon, .menu-element .checkbox-menu ul li:hover .icon,
.menu-element .super-menu ul li:hover .icon {
color: #3dcfff; }
/* line 94, ../sass/controls/_menus.scss */
/* line 99, ../sass/controls/_menus.scss */
.menu-element .checkbox-menu ul li {
padding-left: 50px; }
/* line 101, ../sass/controls/_menus.scss */
.menu-element .checkbox-menu ul li .checkbox {
position: absolute;
left: 5px;
top: 0.53333rem; }
/* line 106, ../sass/controls/_menus.scss */
.menu-element .checkbox-menu ul li .checkbox em {
height: 0.7rem;
width: 0.7rem; }
/* line 109, ../sass/controls/_menus.scss */
.menu-element .checkbox-menu ul li .checkbox em:before {
font-size: 7px !important;
height: 0.7rem;
width: 0.7rem;
line-height: 0.7rem; }
/* line 117, ../sass/controls/_menus.scss */
.menu-element .checkbox-menu ul li .type-icon {
left: 25px; }
/* line 123, ../sass/controls/_menus.scss */
.menu-element .super-menu {
display: block;
width: 500px;
height: 480px; }
/* line 104, ../sass/controls/_menus.scss */
/* line 133, ../sass/controls/_menus.scss */
.menu-element .super-menu .contents {
overflow: hidden;
position: absolute;
@ -2923,12 +2951,12 @@ label.checkbox.custom {
left: 5px;
width: auto;
height: auto; }
/* line 107, ../sass/controls/_menus.scss */
/* line 136, ../sass/controls/_menus.scss */
.menu-element .super-menu .pane {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box; }
/* line 109, ../sass/controls/_menus.scss */
/* line 138, ../sass/controls/_menus.scss */
.menu-element .super-menu .pane.left {
border-right: 1px solid rgba(255, 255, 255, 0.2);
left: 0;
@ -2937,23 +2965,23 @@ label.checkbox.custom {
width: 50%;
overflow-x: hidden;
overflow-y: auto; }
/* line 119, ../sass/controls/_menus.scss */
/* line 148, ../sass/controls/_menus.scss */
.menu-element .super-menu .pane.left ul li {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
padding-left: 30px;
border-top: none; }
/* line 126, ../sass/controls/_menus.scss */
/* line 155, ../sass/controls/_menus.scss */
.menu-element .super-menu .pane.right {
left: auto;
right: 0;
padding: 25px;
width: 50%; }
/* line 132, ../sass/controls/_menus.scss */
/* line 161, ../sass/controls/_menus.scss */
.menu-element .super-menu .pane.right .icon {
color: #fff; }
/* line 139, ../sass/controls/_menus.scss */
/* line 168, ../sass/controls/_menus.scss */
.menu-element .super-menu .menu-item-description .desc-area.icon {
position: relative;
font-size: 8em;
@ -2962,40 +2990,40 @@ label.checkbox.custom {
line-height: 150px;
margin-bottom: 25px;
text-align: center; }
/* line 149, ../sass/controls/_menus.scss */
/* line 178, ../sass/controls/_menus.scss */
.menu-element .super-menu .menu-item-description .desc-area.title {
color: #fff;
font-size: 1.2em;
margin-bottom: 0.5em; }
/* line 154, ../sass/controls/_menus.scss */
/* line 183, ../sass/controls/_menus.scss */
.menu-element .super-menu .menu-item-description .desc-area.description {
color: #fff;
font-size: 0.8em;
line-height: 1.5em; }
/* line 163, ../sass/controls/_menus.scss */
.menu-element .context-menu {
/* line 192, ../sass/controls/_menus.scss */
.menu-element .context-menu, .menu-element .checkbox-menu {
font-size: 0.80rem; }
/* line 168, ../sass/controls/_menus.scss */
/* line 197, ../sass/controls/_menus.scss */
.context-menu-holder {
pointer-events: none;
position: absolute;
height: 200px;
width: 170px;
z-index: 70; }
/* line 174, ../sass/controls/_menus.scss */
/* line 203, ../sass/controls/_menus.scss */
.context-menu-holder .context-menu-wrapper {
position: absolute;
height: 100%;
width: 100%; }
/* line 181, ../sass/controls/_menus.scss */
.context-menu-holder.go-left .context-menu {
/* line 210, ../sass/controls/_menus.scss */
.context-menu-holder.go-left .context-menu, .context-menu-holder.go-left .menu-element .checkbox-menu, .menu-element .context-menu-holder.go-left .checkbox-menu {
right: 0; }
/* line 182, ../sass/controls/_menus.scss */
.context-menu-holder.go-up .context-menu {
/* line 213, ../sass/controls/_menus.scss */
.context-menu-holder.go-up .context-menu, .context-menu-holder.go-up .menu-element .checkbox-menu, .menu-element .context-menu-holder.go-up .checkbox-menu {
bottom: 0; }
/* line 185, ../sass/controls/_menus.scss */
/* line 218, ../sass/controls/_menus.scss */
.btn-bar.right .menu,
.menus-to-left .menu {
left: auto;
@ -4793,30 +4821,30 @@ input[type="text"] {
.l-infobubble-wrapper .l-infobubble table tr td {
padding: 2px 0;
vertical-align: top; }
/* line 57, ../sass/helpers/_bubbles.scss */
/* line 53, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper .l-infobubble table tr td.label {
padding-right: 10px;
white-space: nowrap; }
/* line 61, ../sass/helpers/_bubbles.scss */
/* line 57, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper .l-infobubble table tr td.value {
white-space: nowrap; }
/* line 65, ../sass/helpers/_bubbles.scss */
word-break: break-all; }
/* line 61, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper .l-infobubble table tr td.align-wrap {
white-space: normal; }
/* line 71, ../sass/helpers/_bubbles.scss */
/* line 67, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper .l-infobubble .title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 5px; }
/* line 78, ../sass/helpers/_bubbles.scss */
/* line 74, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-left {
margin-left: 20px; }
/* line 80, ../sass/helpers/_bubbles.scss */
/* line 76, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-left .l-infobubble::before {
right: 100%; }
@media screen and (orientation: portrait) and (min-width: 515px) and (max-width: 799px) and (min-height: 741px) and (max-height: 1024px) and (max-device-width: 799px) and (max-device-height: 1024px), screen and (orientation: landscape) and (min-height: 515px) and (max-height: 799px) and (min-width: 741px) and (max-width: 1024px) and (max-device-width: 799px) and (max-device-height: 1024px), screen and (orientation: landscape) and (min-height: 515px) and (max-height: 799px) and (min-width: 741px) and (max-width: 1024px) and (max-device-width: 1024px) and (max-device-height: 799px), screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 80, ../sass/helpers/_bubbles.scss */
/* line 76, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-left .l-infobubble::before {
width: 0;
height: 0;
@ -4824,30 +4852,30 @@ input[type="text"] {
border-bottom: 6.66667px solid transparent;
border-right: 10px solid #ddd; } }
@media screen and (orientation: portrait) and (min-width: 515px) and (max-width: 799px) and (min-height: 741px) and (max-height: 1024px) and (max-device-width: 799px) and (max-device-height: 1024px), screen and (orientation: landscape) and (min-height: 515px) and (max-height: 799px) and (min-width: 741px) and (max-width: 1024px) and (max-device-width: 799px) and (max-device-height: 1024px), screen and (orientation: landscape) and (min-height: 515px) and (max-height: 799px) and (min-width: 741px) and (max-width: 1024px) and (max-device-width: 1024px) and (max-device-height: 799px), screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 92, ../sass/helpers/_bubbles.scss */
/* line 88, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-right {
margin-right: 20px; } }
/* line 99, ../sass/helpers/_bubbles.scss */
/* line 95, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-right .l-infobubble::before {
left: 100%; }
@media screen and (orientation: portrait) and (min-width: 515px) and (max-width: 799px) and (min-height: 741px) and (max-height: 1024px) and (max-device-width: 799px) and (max-device-height: 1024px), screen and (orientation: landscape) and (min-height: 515px) and (max-height: 799px) and (min-width: 741px) and (max-width: 1024px) and (max-device-width: 799px) and (max-device-height: 1024px), screen and (orientation: landscape) and (min-height: 515px) and (max-height: 799px) and (min-width: 741px) and (max-width: 1024px) and (max-device-width: 1024px) and (max-device-height: 799px), screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 99, ../sass/helpers/_bubbles.scss */
/* line 95, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-right .l-infobubble::before {
width: 0;
height: 0;
border-top: 6.66667px solid transparent;
border-bottom: 6.66667px solid transparent;
border-left: 10px solid #ddd; } }
/* line 112, ../sass/helpers/_bubbles.scss */
/* line 108, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-top .l-infobubble::before {
top: 20px; }
/* line 118, ../sass/helpers/_bubbles.scss */
/* line 114, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-btm .l-infobubble::before {
bottom: 20px; }
/* line 123, ../sass/helpers/_bubbles.scss */
/* line 119, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-down {
margin-bottom: 10px; }
/* line 125, ../sass/helpers/_bubbles.scss */
/* line 121, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-down .l-infobubble::before {
left: 50%;
top: 100%;
@ -4855,21 +4883,21 @@ input[type="text"] {
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7.5px solid #ddd; }
/* line 134, ../sass/helpers/_bubbles.scss */
/* line 130, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper .arw {
z-index: 2; }
/* line 137, ../sass/helpers/_bubbles.scss */
/* line 133, ../sass/helpers/_bubbles.scss */
.l-infobubble-wrapper.arw-up .arw.arw-down, .l-infobubble-wrapper.arw-down .arw.arw-up {
display: none; }
/* line 144, ../sass/helpers/_bubbles.scss */
/* line 142, ../sass/helpers/_bubbles.scss */
.l-thumbsbubble-wrapper .arw-up {
width: 0;
height: 0;
border-left: 6.66667px solid transparent;
border-right: 6.66667px solid transparent;
border-bottom: 10px solid #4d4d4d; }
/* line 147, ../sass/helpers/_bubbles.scss */
/* line 145, ../sass/helpers/_bubbles.scss */
.l-thumbsbubble-wrapper .arw-down {
width: 0;
height: 0;
@ -4877,7 +4905,7 @@ input[type="text"] {
border-right: 6.66667px solid transparent;
border-top: 10px solid #4d4d4d; }
/* line 151, ../sass/helpers/_bubbles.scss */
/* line 150, ../sass/helpers/_bubbles.scss */
.s-infobubble {
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
@ -4888,22 +4916,29 @@ input[type="text"] {
background: #ddd;
color: #666;
font-size: 0.8rem; }
/* line 158, ../sass/helpers/_bubbles.scss */
/* line 157, ../sass/helpers/_bubbles.scss */
.s-infobubble .title {
color: #333333;
font-weight: bold; }
/* line 163, ../sass/helpers/_bubbles.scss */
.s-infobubble tr td {
border-top: 1px solid #c4c4c4;
.s-infobubble table tr td {
border: none;
border-top: 1px solid #c4c4c4 !important;
font-size: 0.9em; }
/* line 167, ../sass/helpers/_bubbles.scss */
.s-infobubble tr:first-child td {
/* line 169, ../sass/helpers/_bubbles.scss */
.s-infobubble table tr:first-child td {
border-top: none !important; }
/* line 174, ../sass/helpers/_bubbles.scss */
.s-infobubble:first-child td {
border-top: none; }
/* line 171, ../sass/helpers/_bubbles.scss */
/* line 178, ../sass/helpers/_bubbles.scss */
.s-infobubble .label {
color: gray; }
/* line 182, ../sass/helpers/_bubbles.scss */
.s-infobubble .value {
color: #333333; }
/* line 176, ../sass/helpers/_bubbles.scss */
/* line 188, ../sass/helpers/_bubbles.scss */
.s-thumbsbubble {
background: #4d4d4d;
color: #b3b3b3; }

@ -304,6 +304,252 @@ ul.tree {
ul.tree ul.tree {
margin-left: 15px; }
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* line 23, ../sass/search/_search.scss */
.abs.search-holder {
height: 25px;
bottom: 0;
top: 23px;
z-index: 5; }
/* line 27, ../sass/search/_search.scss */
.abs.search-holder.active {
height: auto;
bottom: 0; }
/* line 38, ../sass/search/_search.scss */
.search {
display: flex;
flex-direction: column;
height: 100%; }
/* line 49, ../sass/search/_search.scss */
.search .search-bar {
font-size: 0.8em;
position: relative;
width: 100%; }
/* line 66, ../sass/search/_search.scss */
.search .search-bar .search-input {
height: 25px;
line-height: 25px;
padding-top: 0;
padding-bottom: 0; }
/* line 73, ../sass/search/_search.scss */
.search .search-bar .search-icon,
.search .search-bar .clear-icon,
.search .search-bar .menu-icon {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #737373;
height: 17px;
width: 17px;
line-height: 17px;
position: absolute;
text-align: center;
top: 4px; }
/* line 86, ../sass/search/_search.scss */
.search .search-bar .clear-icon,
.search .search-bar .menu-icon {
cursor: pointer;
transition: color .25s; }
/* line 93, ../sass/search/_search.scss */
.search .search-bar .search-input {
position: relative;
width: 100%;
padding-left: 22px !important;
padding-right: 44px !important; }
/* line 100, ../sass/search/_search.scss */
.search .search-bar .search-input input {
width: 100%; }
/* line 105, ../sass/search/_search.scss */
.search .search-bar .search-icon {
color: #737373;
left: 3px;
transition: visibility .15s, opacity .15s, color .2s;
pointer-events: none; }
/* line 125, ../sass/search/_search.scss */
.search .search-bar .search-input:hover + div.search-icon {
color: #a6a6a6; }
/* line 129, ../sass/search/_search.scss */
.search .search-bar .clear-icon {
right: 22px;
visibility: hidden;
opacity: 0;
transition: visibility .15s, opacity .15s, color .2s; }
/* line 138, ../sass/search/_search.scss */
.search .search-bar .clear-icon.content {
visibility: visible;
opacity: 1; }
/* line 143, ../sass/search/_search.scss */
.search .search-bar .clear-icon:hover {
color: #a6a6a6; }
/* line 148, ../sass/search/_search.scss */
.search .search-bar .menu-icon {
font-size: 0.8em;
padding-right: 4px;
right: 4px;
text-align: right; }
/* line 154, ../sass/search/_search.scss */
.search .search-bar .menu-icon:hover {
color: #a6a6a6; }
/* line 159, ../sass/search/_search.scss */
.search .search-bar .search-menu-holder {
float: right;
left: -20px;
z-index: 1;
transition: visibility .05s, opacity .05s; }
/* line 169, ../sass/search/_search.scss */
.search .search-bar .search-menu-holder.off {
visibility: hidden;
opacity: 0; }
/* line 176, ../sass/search/_search.scss */
.search .search-bar .menu-icon:hover + div.search-menu-holder {
visibility: visible; }
/* line 179, ../sass/search/_search.scss */
.search .search-bar div.search-menu-holder:hover {
visibility: visible; }
/* line 184, ../sass/search/_search.scss */
.search .active-filter-display {
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
line-height: 130%;
padding: 5px 0;
padding-left: 1.4625em;
font-size: 0.65em;
margin-top: 3px; }
/* line 199, ../sass/search/_search.scss */
.search .active-filter-display .clear-filters-icon {
opacity: 0.4;
font-size: 0.8em;
position: absolute;
left: 1px;
cursor: pointer; }
/* line 210, ../sass/search/_search.scss */
.search .active-filter-display.off {
visibility: hidden;
opacity: 0;
height: 0;
margin: 0;
padding: 0;
border: 0; }
/* line 220, ../sass/search/_search.scss */
.search .search-scroll {
order: 3;
margin-top: 4px;
overflow-y: auto;
top: auto;
height: auto;
max-height: 100%;
position: relative; }
/* line 235, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item {
-moz-transition: background-color 0.25s;
-o-transition: background-color 0.25s;
-webkit-transition: background-color 0.25s;
transition: background-color 0.25s;
margin-bottom: 2px;
border-radius: 2px;
padding-top: 4px;
padding-bottom: 2px; }
/* line 249, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item .label {
margin-left: 6px; }
/* line 253, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item .label .title-label {
display: inline-block;
position: absolute;
left: 29px;
right: 5px;
font-size: .8em;
line-height: 17px;
width: auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
/* line 275, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item.selected {
background: #005177;
color: #fff; }
/* line 279, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item.selected .view-control {
color: #0099cc; }
/* line 282, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item.selected .label .type-icon {
color: #fff; }
/* line 287, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item .label .type-icon .l-icon-link {
text-shadow: black 0 1px 2px;
z-index: 2;
color: #49dedb;
font-size: 8px;
line-height: 8px;
height: 8px;
width: 8px;
margin-left: -25px; }
/* line 296, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item:not(.selected):hover {
background: #404040;
color: #cccccc; }
/* line 299, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item:not(.selected):hover .context-trigger {
display: block; }
/* line 302, ../sass/search/_search.scss */
.search .search-scroll .results .search-result-item:not(.selected):hover .icon {
color: #33ccff; }
/* line 310, ../sass/search/_search.scss */
.search .search-scroll .load-icon {
position: relative; }
/* line 312, ../sass/search/_search.scss */
.search .search-scroll .load-icon.loading {
pointer-events: none;
margin-left: 6px; }
/* line 316, ../sass/search/_search.scss */
.search .search-scroll .load-icon.loading .title-label {
font-style: italic;
font-size: .9em;
opacity: 0.5;
margin-left: 26px;
line-height: 24px; }
/* line 326, ../sass/search/_search.scss */
.search .search-scroll .load-icon.loading .wait-spinner {
margin-left: 6px; }
/* line 331, ../sass/search/_search.scss */
.search .search-scroll .load-icon:not(.loading) {
cursor: pointer; }
/* line 336, ../sass/search/_search.scss */
.search .search-scroll .load-more-button {
margin-top: 5px 0;
font-size: 0.8em;
position: relative;
left: 50%;
margin-left: -45px;
text-align: center;
width: 90px;
white-space: nowrap; }
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space

@ -1,19 +1,27 @@
{
"metadata": {
"name": "WTD Symbols v2.1",
"lastOpened": 1439844340068,
"created": 1439844318831
"name": "WTD Symbols v2.2",
"lastOpened": 1439948744240,
"created": 1439948734037
},
"iconSets": [
{
"selection": [
{
"order": 82,
"id": 84,
"prevSize": 32,
"code": 58887,
"name": "icon-x-in-circle",
"tempChar": ""
},
{
"order": 77,
"id": 83,
"prevSize": 32,
"code": 58881,
"name": "icon-datatable",
"tempChar": ""
"tempChar": ""
},
{
"order": 78,
@ -21,7 +29,7 @@
"prevSize": 32,
"code": 58882,
"name": "icon-tabular-scrolling",
"tempChar": ""
"tempChar": ""
},
{
"order": 79,
@ -29,7 +37,7 @@
"prevSize": 32,
"code": 58884,
"name": "icon-tabular",
"tempChar": ""
"tempChar": ""
},
{
"order": 80,
@ -37,7 +45,7 @@
"prevSize": 32,
"code": 58885,
"name": "icon-calendar",
"tempChar": ""
"tempChar": ""
},
{
"order": 81,
@ -45,7 +53,7 @@
"prevSize": 32,
"code": 58886,
"name": "icon-paint-bucket",
"tempChar": ""
"tempChar": ""
},
{
"order": 1,
@ -53,7 +61,7 @@
"prevSize": 32,
"code": 123,
"name": "icon-pointer-left",
"tempChar": ""
"tempChar": ""
},
{
"order": 3,
@ -61,7 +69,7 @@
"prevSize": 32,
"code": 125,
"name": "icon-pointer-right",
"tempChar": ""
"tempChar": ""
},
{
"order": 4,
@ -69,7 +77,7 @@
"prevSize": 32,
"code": 80,
"name": "icon-person",
"tempChar": ""
"tempChar": ""
},
{
"order": 5,
@ -77,7 +85,7 @@
"prevSize": 32,
"code": 232,
"name": "icon-chain-links",
"tempChar": ""
"tempChar": ""
},
{
"order": 6,
@ -85,7 +93,7 @@
"prevSize": 32,
"code": 115,
"name": "icon-database-in-brackets",
"tempChar": ""
"tempChar": ""
},
{
"order": 7,
@ -93,7 +101,7 @@
"prevSize": 32,
"code": 114,
"name": "icon-refresh",
"tempChar": ""
"tempChar": ""
},
{
"order": 8,
@ -101,7 +109,7 @@
"prevSize": 32,
"code": 108,
"name": "icon-lock",
"tempChar": ""
"tempChar": ""
},
{
"order": 9,
@ -109,7 +117,7 @@
"prevSize": 32,
"code": 51,
"name": "icon-box-with-dashed-lines",
"tempChar": ""
"tempChar": ""
},
{
"order": 10,
@ -117,7 +125,7 @@
"prevSize": 32,
"code": 58880,
"name": "icon-box-with-arrow-cursor",
"tempChar": ""
"tempChar": ""
},
{
"order": 11,
@ -125,7 +133,7 @@
"prevSize": 32,
"code": 65,
"name": "icon-activity-mode",
"tempChar": ""
"tempChar": ""
},
{
"order": 12,
@ -133,7 +141,7 @@
"prevSize": 32,
"code": 97,
"name": "icon-activity",
"tempChar": ""
"tempChar": ""
},
{
"order": 13,
@ -141,7 +149,7 @@
"prevSize": 32,
"code": 33,
"name": "icon-alert-rect",
"tempChar": ""
"tempChar": ""
},
{
"order": 14,
@ -149,7 +157,7 @@
"prevSize": 32,
"code": 58883,
"name": "icon-alert-triangle",
"tempChar": ""
"tempChar": ""
},
{
"order": 15,
@ -157,7 +165,7 @@
"prevSize": 32,
"code": 238,
"name": "icon-arrow-double-down",
"tempChar": ""
"tempChar": ""
},
{
"order": 16,
@ -165,7 +173,7 @@
"prevSize": 32,
"code": 235,
"name": "icon-arrow-double-up",
"tempChar": ""
"tempChar": ""
},
{
"order": 2,
@ -173,7 +181,7 @@
"prevSize": 32,
"code": 118,
"name": "icon-arrow-down",
"tempChar": ""
"tempChar": ""
},
{
"order": 19,
@ -181,7 +189,7 @@
"prevSize": 32,
"code": 60,
"name": "icon-arrow-left",
"tempChar": ""
"tempChar": ""
},
{
"order": 20,
@ -189,7 +197,7 @@
"prevSize": 32,
"code": 62,
"name": "icon-arrow-right",
"tempChar": ""
"tempChar": ""
},
{
"order": 21,
@ -197,7 +205,7 @@
"prevSize": 32,
"code": 236,
"name": "icon-arrow-tall-down",
"tempChar": ""
"tempChar": ""
},
{
"order": 22,
@ -205,7 +213,7 @@
"prevSize": 32,
"code": 237,
"name": "icon-arrow-tall-up",
"tempChar": ""
"tempChar": ""
},
{
"order": 23,
@ -213,7 +221,7 @@
"prevSize": 32,
"code": 94,
"name": "icon-arrow-up",
"tempChar": ""
"tempChar": ""
},
{
"order": 24,
@ -221,7 +229,7 @@
"prevSize": 32,
"code": 73,
"name": "icon-arrows-out",
"tempChar": ""
"tempChar": ""
},
{
"order": 25,
@ -229,7 +237,7 @@
"prevSize": 32,
"code": 58893,
"name": "icon-arrows-right-left",
"tempChar": ""
"tempChar": ""
},
{
"order": 33,
@ -237,7 +245,7 @@
"prevSize": 32,
"code": 53,
"name": "icon-arrows-up-down",
"tempChar": ""
"tempChar": ""
},
{
"order": 26,
@ -245,7 +253,7 @@
"prevSize": 32,
"code": 42,
"name": "icon-asterisk",
"tempChar": ""
"tempChar": ""
},
{
"order": 27,
@ -253,7 +261,7 @@
"prevSize": 32,
"code": 72,
"name": "icon-autoflow-tabular",
"tempChar": ""
"tempChar": ""
},
{
"order": 28,
@ -261,7 +269,7 @@
"prevSize": 32,
"code": 224,
"name": "icon-box-round-corners",
"tempChar": ""
"tempChar": ""
},
{
"order": 29,
@ -269,7 +277,7 @@
"prevSize": 32,
"code": 50,
"name": "icon-check",
"tempChar": ""
"tempChar": ""
},
{
"order": 30,
@ -277,7 +285,7 @@
"prevSize": 32,
"code": 67,
"name": "icon-clock",
"tempChar": ""
"tempChar": ""
},
{
"order": 31,
@ -285,7 +293,7 @@
"prevSize": 32,
"code": 46,
"name": "icon-connectivity",
"tempChar": ""
"tempChar": ""
},
{
"order": 32,
@ -293,7 +301,7 @@
"prevSize": 32,
"code": 100,
"name": "icon-database-query",
"tempChar": ""
"tempChar": ""
},
{
"order": 17,
@ -301,7 +309,7 @@
"prevSize": 32,
"code": 68,
"name": "icon-database",
"tempChar": ""
"tempChar": ""
},
{
"order": 35,
@ -309,7 +317,7 @@
"prevSize": 32,
"code": 81,
"name": "icon-dictionary",
"tempChar": ""
"tempChar": ""
},
{
"order": 36,
@ -317,7 +325,7 @@
"prevSize": 32,
"code": 242,
"name": "icon-duplicate",
"tempChar": ""
"tempChar": ""
},
{
"order": 37,
@ -325,7 +333,7 @@
"prevSize": 32,
"code": 102,
"name": "icon-folder-new",
"tempChar": ""
"tempChar": ""
},
{
"order": 38,
@ -333,7 +341,7 @@
"prevSize": 32,
"code": 70,
"name": "icon-folder",
"tempChar": ""
"tempChar": ""
},
{
"order": 39,
@ -341,7 +349,7 @@
"prevSize": 32,
"code": 95,
"name": "icon-fullscreen-collapse",
"tempChar": ""
"tempChar": ""
},
{
"order": 40,
@ -349,7 +357,7 @@
"prevSize": 32,
"code": 122,
"name": "icon-fullscreen-expand",
"tempChar": ""
"tempChar": ""
},
{
"order": 41,
@ -357,7 +365,7 @@
"prevSize": 32,
"code": 71,
"name": "icon-gear",
"tempChar": ""
"tempChar": ""
},
{
"order": 49,
@ -365,7 +373,7 @@
"prevSize": 32,
"code": 227,
"name": "icon-image",
"tempChar": ""
"tempChar": ""
},
{
"order": 42,
@ -373,7 +381,7 @@
"prevSize": 32,
"code": 225,
"name": "icon-layers",
"tempChar": ""
"tempChar": ""
},
{
"order": 43,
@ -381,7 +389,7 @@
"prevSize": 32,
"code": 76,
"name": "icon-layout",
"tempChar": ""
"tempChar": ""
},
{
"order": 44,
@ -389,7 +397,7 @@
"prevSize": 32,
"code": 226,
"name": "icon-line-horz",
"tempChar": ""
"tempChar": ""
},
{
"order": 75,
@ -397,7 +405,7 @@
"prevSize": 32,
"code": 244,
"name": "icon-link",
"tempChar": ""
"tempChar": ""
},
{
"order": 46,
@ -405,7 +413,7 @@
"prevSize": 32,
"code": 88,
"name": "icon-magnify-in",
"tempChar": ""
"tempChar": ""
},
{
"order": 47,
@ -413,7 +421,7 @@
"prevSize": 32,
"code": 89,
"name": "icon-magnify-out",
"tempChar": ""
"tempChar": ""
},
{
"order": 48,
@ -421,7 +429,7 @@
"prevSize": 32,
"code": 77,
"name": "icon-magnify",
"tempChar": ""
"tempChar": ""
},
{
"order": 34,
@ -429,7 +437,7 @@
"prevSize": 32,
"code": 109,
"name": "icon-menu",
"tempChar": ""
"tempChar": ""
},
{
"order": 50,
@ -437,7 +445,7 @@
"prevSize": 32,
"code": 243,
"name": "icon-move",
"tempChar": ""
"tempChar": ""
},
{
"order": 51,
@ -445,7 +453,7 @@
"prevSize": 32,
"code": 121,
"name": "icon-new-window",
"tempChar": ""
"tempChar": ""
},
{
"order": 52,
@ -453,7 +461,7 @@
"prevSize": 32,
"code": 111,
"name": "icon-object",
"tempChar": ""
"tempChar": ""
},
{
"order": 73,
@ -461,7 +469,7 @@
"prevSize": 32,
"code": 63,
"name": "icon-object-unknown",
"tempChar": ""
"tempChar": ""
},
{
"order": 53,
@ -469,7 +477,7 @@
"prevSize": 32,
"code": 86,
"name": "icon-packet",
"tempChar": ""
"tempChar": ""
},
{
"order": 54,
@ -477,7 +485,7 @@
"prevSize": 32,
"code": 234,
"name": "icon-page",
"tempChar": ""
"tempChar": ""
},
{
"order": 55,
@ -485,7 +493,7 @@
"prevSize": 32,
"code": 241,
"name": "icon-pause",
"tempChar": ""
"tempChar": ""
},
{
"order": 56,
@ -493,7 +501,7 @@
"prevSize": 32,
"code": 112,
"name": "icon-pencil",
"tempChar": ""
"tempChar": ""
},
{
"order": 65,
@ -501,7 +509,7 @@
"prevSize": 32,
"code": 79,
"name": "icon-people",
"tempChar": ""
"tempChar": ""
},
{
"order": 57,
@ -509,7 +517,7 @@
"prevSize": 32,
"code": 239,
"name": "icon-play",
"tempChar": ""
"tempChar": ""
},
{
"order": 58,
@ -517,7 +525,7 @@
"prevSize": 32,
"code": 233,
"name": "icon-plot-resource",
"tempChar": ""
"tempChar": ""
},
{
"order": 59,
@ -525,7 +533,7 @@
"prevSize": 32,
"code": 43,
"name": "icon-plus",
"tempChar": ""
"tempChar": ""
},
{
"order": 60,
@ -533,7 +541,7 @@
"prevSize": 32,
"code": 45,
"name": "icon-minus",
"tempChar": ""
"tempChar": ""
},
{
"order": 61,
@ -541,7 +549,7 @@
"prevSize": 32,
"code": 54,
"name": "icon-sine",
"tempChar": ""
"tempChar": ""
},
{
"order": 62,
@ -549,7 +557,7 @@
"prevSize": 32,
"code": 228,
"name": "icon-T",
"tempChar": ""
"tempChar": ""
},
{
"order": 63,
@ -557,7 +565,7 @@
"prevSize": 32,
"code": 116,
"name": "icon-telemetry-panel",
"tempChar": ""
"tempChar": ""
},
{
"order": 64,
@ -565,7 +573,7 @@
"prevSize": 32,
"code": 84,
"name": "icon-telemetry",
"tempChar": ""
"tempChar": ""
},
{
"order": 18,
@ -573,7 +581,7 @@
"prevSize": 32,
"code": 246,
"name": "icon-thumbs-strip",
"tempChar": ""
"tempChar": ""
},
{
"order": 67,
@ -581,7 +589,7 @@
"prevSize": 32,
"code": 83,
"name": "icon-timeline",
"tempChar": ""
"tempChar": ""
},
{
"order": 68,
@ -589,7 +597,7 @@
"prevSize": 32,
"code": 245,
"name": "icon-timer",
"tempChar": ""
"tempChar": ""
},
{
"order": 69,
@ -597,7 +605,7 @@
"prevSize": 32,
"code": 90,
"name": "icon-trash",
"tempChar": ""
"tempChar": ""
},
{
"order": 70,
@ -605,7 +613,7 @@
"prevSize": 32,
"code": 229,
"name": "icon-two-parts-both",
"tempChar": ""
"tempChar": ""
},
{
"order": 71,
@ -613,7 +621,7 @@
"prevSize": 32,
"code": 231,
"name": "icon-two-parts-one-only",
"tempChar": ""
"tempChar": ""
},
{
"order": 72,
@ -621,7 +629,7 @@
"prevSize": 32,
"code": 120,
"name": "icon-x-heavy",
"tempChar": ""
"tempChar": ""
},
{
"order": 66,
@ -629,7 +637,7 @@
"prevSize": 32,
"code": 58946,
"name": "icon-x",
"tempChar": ""
"tempChar": ""
}
],
"id": 2,
@ -644,6 +652,21 @@
"height": 1024,
"prevSize": 32,
"icons": [
{
"id": 84,
"paths": [
"M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM832 704l-128 128-192-192-192 192-128-128 192-192-192-192 128-128 192 192 192-192 128 128-192 192 192 192z"
],
"attrs": [],
"isMulticolor": false,
"grid": 0,
"tags": [
"icon-x-in-circle"
],
"colorPermutations": {
"16161751": []
}
},
{
"id": 83,
"paths": [
@ -666,6 +689,10 @@
"icon-datatable"
],
"colorPermutations": {
"16161751": [
1,
1
],
"125525525516161751": [
1,
1
@ -709,6 +736,13 @@
"icon-tabular-scrolling"
],
"colorPermutations": {
"16161751": [
1,
1,
1,
1,
1
],
"125525525516161751": [
1,
1,
@ -760,6 +794,14 @@
"icon-tabular"
],
"colorPermutations": {
"16161751": [
0,
0,
0,
0,
0,
0
],
"125525525516161751": [
0,
0,
@ -787,6 +829,9 @@
"icon-calendar"
],
"colorPermutations": {
"16161751": [
1
],
"125525525516161751": [
1
]
@ -814,6 +859,10 @@
"icon-paint-bucket"
],
"colorPermutations": {
"16161751": [
1,
1
],
"125525525516161751": [
1,
1

@ -102,6 +102,7 @@ $ueAppLogoW: 105px;
$ueEditToolBarH: 25px;
$ueBrowseLeftPaneW: 25%;
$ueEditLeftPaneW: 75%;
$treeSearchInputBarH: 25px;
// Overlay
$ovrTopBarH: 60px;
$ovrFooterH: 40px;

@ -1,3 +1,4 @@
/*****************************************************************************
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@ -34,7 +35,6 @@
@import "fonts";
@import "user-environ/layout";
@import "mobile/layout";
@import "fixed-position";
@import "about";
@import "text";

@ -21,15 +21,15 @@
*****************************************************************************/
/******************************************************** MENUS */
.menu-element {
$bg: lighten($colorBodyBg, 5%);
$bg: lighten($colorBodyBg, 5%);
$bgHover: lighten($bg, 20%);
$iconColor: $colorKey;
cursor: pointer;
position: relative;
.menu {
@include border-radius($basicCr);
@include containerSubtle($bg);
@include txtShdwSubtle(0.2);
@include containerSubtle($bg);
@include txtShdwSubtle(0.2);
display: block; // set to block via jQuery
padding: $interiorMarginSm 0;
position: absolute;
@ -37,11 +37,12 @@
ul {
@include menuUlReset();
li {
@include box-sizing(border-box);
@include box-sizing(border-box);
border-top: 1px solid lighten($bg, 20%);
color: lighten($bg, 60%);
line-height: $menuLineH;
padding: $interiorMarginSm $interiorMargin * 2 $interiorMarginSm ($interiorMargin * 2) + $treeTypeIconW;
position: relative;
white-space: nowrap;
&:first-child {
border: none;
@ -61,7 +62,7 @@
// display: block;
//}
.type-icon {
left: $interiorMargin * 2;
left: $interiorMargin * 2;
}
}
}
@ -77,7 +78,9 @@
@include containerSubtle($bg);
ul li {
padding-left: 25px;
a { color: $fg; }
a {
color: $fg;
}
.icon {
color: $ic;
}
@ -90,99 +93,131 @@
}
}
.checkbox-menu {
// Used in search dropdown in tree
@extend .context-menu;
ul li {
padding-left: 50px;
.checkbox {
$d: 0.7rem;
position: absolute;
left: $interiorMargin;
top: ($menuLineH - $d) / 1.5;
em {
height: $d;
width: $d;
&:before {
font-size: 7px !important;// $d/2;
height: $d;
width: $d;
line-height: $d;
}
}
}
.type-icon {
left: 25px;
}
}
}
.super-menu {
$w: 500px;
$h: $w - 20;
$plw: 50%; //$w * 0.5;
$prw: 50%; //$w - $plw;
$fg: #fff; //lighten($colorBodyFg, 40%);
$bgHover: $colorKey; //$bg;
display: block;
width: $w;
height: $h;
.contents {
@include absPosDefault($interiorMargin);
}
.pane {
@include box-sizing(border-box);
&.left {
//@include test();
border-right: 1px solid rgba(white, 0.2);
left: 0;
padding-right: $interiorMargin;
right: auto;
width: $plw;
overflow-x: hidden;
overflow-y: auto;
ul {
li {
@include border-radius($controlCr);
padding-left: 30px;
border-top: none;
}
}
}
&.right {
//@include test(red);
left: auto;
right: 0;
padding: $interiorMargin * 5;
width: $prw;
.icon {
color: $fg;
}
}
}
.menu-item-description {
.desc-area {
&.icon {
$h: 150px;
position: relative;
font-size: 8em;
left: 0;
height: $h;
line-height: $h;
margin-bottom: $interiorMargin * 5;
text-align: center;
}
&.title {
color: $fg;
font-size: 1.2em;
margin-bottom: 0.5em;
}
&.description {
//color: lighten($bg, 30%);
color: $fg;
font-size: 0.8em;
line-height: 1.5em;
}
}
}
}
.context-menu {
font-size: 0.80rem;
}
.super-menu {
$w: 500px;
$h: $w - 20;
$plw: 50%; //$w * 0.5;
$prw: 50%; //$w - $plw;
$fg: #fff; //lighten($colorBodyFg, 40%);
$bgHover: $colorKey; //$bg;
display: block;
width: $w;
height: $h;
.contents {
@include absPosDefault($interiorMargin);
}
.pane {
@include box-sizing(border-box);
&.left {
//@include test();
border-right: 1px solid rgba(white, 0.2);
left: 0;
padding-right: $interiorMargin;
right: auto;
width: $plw;
overflow-x: hidden;
overflow-y: auto;
ul {
li {
@include border-radius($controlCr);
padding-left: 30px;
border-top: none;
}
}
}
&.right {
//@include test(red);
left: auto;
right: 0;
padding: $interiorMargin * 5;
width: $prw;
.icon {
color: $fg;
}
}
}
.menu-item-description {
.desc-area {
&.icon {
$h: 150px;
position: relative;
font-size: 8em;
left: 0;
height: $h;
line-height: $h;
margin-bottom: $interiorMargin * 5;
text-align: center;
}
&.title {
color: $fg;
font-size: 1.2em;
margin-bottom: 0.5em;
}
&.description {
//color: lighten($bg, 30%);
color: $fg;
font-size: 0.8em;
line-height: 1.5em;
}
}
}
}
.context-menu {
font-size: 0.80rem;
}
}
.context-menu-holder {
pointer-events: none;
position: absolute;
height: 200px;
width: 170px;
z-index: 70;
.context-menu-wrapper {
position: absolute;
height: 100%;
width: 100%;
.context-menu {
}
}
&.go-left .context-menu { right: 0; }
&.go-up .context-menu { bottom: 0; }
pointer-events: none;
position: absolute;
height: 200px;
width: 170px;
z-index: 70;
.context-menu-wrapper {
position: absolute;
height: 100%;
width: 100%;
.context-menu {
}
}
&.go-left .context-menu {
right: 0;
}
&.go-up .context-menu {
bottom: 0;
}
}
.btn-bar.right .menu,
.menus-to-left .menu {
left: auto; right: 0; width: auto;
left: auto;
right: 0;
width: auto;
}

@ -48,19 +48,15 @@
width: 100%;
tr {
td {
//max-width: 150px;
padding: 2px 0;
vertical-align: top;
//white-space: nowrap;
//overflow: hidden;
//text-overflow: ellipsis;
&.label {
padding-right: $interiorMargin * 2;
white-space: nowrap;
}
&.value {
white-space: nowrap;
//width: 90%;
//word-wrap: break-word; // Doesn't work in <td>?
word-break: break-all;
}
&.align-wrap {
white-space: normal;
@ -135,7 +131,9 @@
z-index: 2;
}
&.arw-up .arw.arw-down,
&.arw-down .arw.arw-up { display: none; }
&.arw-down .arw.arw-up {
display: none;
}
}
//************************************************* LOOK AND FEEL
@ -148,6 +146,7 @@
@include triangle('down', $bubbleArwSize, 1.5, $colorThumbsBubbleBg);
}
}
.s-infobubble {
$emFg: darken($colorInfoBubbleFg, 20%);
@include border-radius($basicCr);
@ -159,18 +158,31 @@
color: $emFg;
font-weight: bold;
}
tr {
td {
border-top: 1px solid darken($colorInfoBubbleBg, 10%);
font-size: 0.9em;
}
&:first-child td {
border-top: none;
table {
tr {
td {
border: none;
border-top: 1px solid darken($colorInfoBubbleBg, 10%) !important;
font-size: 0.9em;
}
&:first-child td {
border-top: none !important;
}
}
}
&:first-child td {
border-top: none;
}
.label {
color: lighten($emFg, 30%);
}
.value {
color: $emFg;
}
}
.s-thumbsbubble {

@ -82,18 +82,21 @@ table {
border-left: none;
}
&.sort {
.icon-sorting:before {
display: inline-block;
&.sort:after {
color: $colorIconLink;
font-family: symbolsfont;
margin-left: 5px;
font-size: 8px;
content: "\ed";
display: inline-block;
margin-left: $interiorMarginSm;
}
&.asc .icon-sorting:before {
content: '0';
}
&.desc .icon-sorting:before {
content: '1';
&.sort.desc:after {
content: "\ec";
}
}
&.sortable {
cursor: pointer;
}
}
td, .td {
border-bottom: 1px solid $tabularColorBorder;

@ -75,7 +75,7 @@
left: 0 !important;
}
}
.browse-showtree {
// NOTE: DISABLED SELECTION
// Selection disabled in both panes

@ -0,0 +1,31 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
// Overrides some styling in user-environ/_layout.scss
.pane {
&.treeview.left {
.tree-holder {
// Want tree holder to start right below the search bar.
top: 60px;
}
}
}

@ -0,0 +1,347 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.abs.search-holder {
//@include test(#990000);
height: $treeSearchInputBarH;
bottom: 0;
&.active {
height: auto;
bottom: 0;
}
top: 23px;
// Align with the top of the divider bar, below create button
//margin-top: 10px; // CH comment out
z-index:5;
}
.search {
$iconWidth: 20px;
$leftMargin: 6px;
$rightPadding: 5px;
//padding-right: $rightPadding;
//@include test();
display: flex; //block;
flex-direction: column;
height: 100%;
.search-bar {
//$heightAdjust: 4px;
$textInputHeight: 19px; // This is equal to the default value, 19px
$iconEdgeM: 4px;
$iconD: $treeSearchInputBarH - ($iconEdgeM*2);
font-size: 0.8em;
//order: 1;
position: relative;
width: 100%;
//height: $textInputHeight;
//margin-top: $heightAdjust;
.search-input,
.search-icon {
}
.search-input {
height: $treeSearchInputBarH;
line-height: $treeSearchInputBarH;
padding-top: 0;
padding-bottom: 0;
}
.search-icon,
.clear-icon,
.menu-icon {
//@include test(#008800);
@include box-sizing(border-box);
color: $colorItemFg;
height: $iconD; width: $iconD;
line-height: $iconD;
position: absolute;
text-align: center;
top: $iconEdgeM;
}
.clear-icon,
.menu-icon {
cursor: pointer;
transition: color .25s;
}
.search-input {
position: relative;
width: 100%;
padding-left: $iconD + $interiorMargin !important;
padding-right: ($iconD * 2) + ($interiorMargin * 2) !important;
// Make work for mct-control textfield
input {
width: 100%;
}
}
.search-icon {
color: $colorItemFg;
left: $interiorMarginSm;
transition: visibility .15s, opacity .15s, color .2s;
pointer-events: none;
&.content {
// Make icon invisible whenever there is text input
//visibility: hidden;
//opacity: 0;
}
}
// Make icon invisible when the text input is focused
.search-input:focus + div.search-icon {
//visibility: hidden;
//opacity: 0;
}
// Make icon lighten when hovering over search bar
.search-input:hover + div.search-icon {
color: lighten($colorItemFg, 20%);
}
.clear-icon {
right: $iconD + $interiorMargin;
// Icon is visible only when there is text input
visibility: hidden;
opacity: 0;
transition: visibility .15s, opacity .15s, color .2s;
&.content {
visibility: visible;
opacity: 1;
}
&:hover {
color: lighten($colorItemFg, 20%);
}
}
.menu-icon {
font-size: 0.8em;
padding-right: $iconEdgeM;
right: $iconEdgeM;
text-align: right;
&:hover {
color: lighten($colorItemFg, 20%);
}
}
.search-menu-holder {
float: right;
//margin-top: $textInputHeight - 2px;
//left: -50px;
left: -20px;
z-index: 1;
transition: visibility .05s, opacity .05s;
&.off {
visibility: hidden;
opacity: 0;
}
}
// Hovering reveals menu
.menu-icon:hover + div.search-menu-holder {
visibility: visible;
}
div.search-menu-holder:hover {
visibility: visible;
}
}
.active-filter-display {
//order: 2;
$s: 0.65em;
$p: $interiorMargin;
@include border-radius($basicCr);
@include box-sizing(border-box);
line-height: 130%;
padding: $p 0;
padding-left: $s * 2.25;
font-size: $s;
//background-color: rgba(#000, 0.3);
//border-radius: $basicCr;
margin-top: $interiorMarginSm;
.clear-filters-icon {
opacity: 0.4;
font-size: 0.8em;
position: absolute;
left: 1px;
cursor: pointer;
}
// Transition looks weird when the results list has none
//transition: visibility .2s, opacity .2s;
&.off {
visibility: hidden;
opacity: 0;
height: 0;
margin: 0;
padding: 0;
border: 0;
}
}
.search-scroll {
order: 3;
//padding-right: $rightPadding;
margin-top: 4px;
// Adjustable scrolling size
overflow-y: auto;
top: auto;
height: auto;
max-height: 100%;
position: relative;
.results {
.search-result-item {
// Include transitions (for the highlights)
@include single-transition(background-color, 0.25s);
// Space the results from each other
margin-bottom: 2px;
// Make the highlights the right color and shape.
// Attempting to match the style in the tree, but
// while having these be compact.
border-radius: 2px;
padding-top: 4px;
padding-bottom: 2px;
.label {
// Give some padding away from the left side
margin-left: $leftMargin;
.title-label {
display: inline-block;
position: absolute;
// Give some padding away from the left side
left: $leftMargin + 3px + $iconWidth;
// and the right side
right: $rightPadding;
// Size and position the text
font-size: .8em;
line-height: 17px;
// Hide overflow text
width: auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
// Change styling when it's selected
&.selected {
$c: #fff;
background: $colorKeySelectedBg;
color: $c;
.view-control {
color: $colorItemTreeIcon;
}
.label .type-icon {
color: #fff;
}
}
.label .type-icon .l-icon-link {
@include txtShdwSubtle(1);
z-index: 2;
@include ancillaryIcon(8px, $colorIconLink);
margin-left: -25px;
}
// Change styling when it's being hovered over
&:not(.selected) {
&:hover {
background: lighten($colorBodyBg, 5%);
color: lighten($colorBodyFg, 20%);
.context-trigger {
display: block;
}
.icon {
color: $colorItemTreeIconHover;
}
}
}
}
}
.load-icon {
position: relative;
&.loading {
pointer-events: none;
margin-left: $leftMargin;
.title-label {
// Text styling
font-style: italic;
font-size: .9em;
opacity: 0.5;
// Text positioning
margin-left: $iconWidth + $leftMargin;
line-height: 24px;
}
.wait-spinner {
margin-left: $leftMargin;
}
}
&:not(.loading) {
cursor: pointer;
}
}
.load-more-button {
margin-top: $interiorMargin 0;
font-size: 0.8em;
position: relative;
left: 50%;
margin-left: -45px;
text-align: center;
width: 90px;
white-space: nowrap;
}
}
}

@ -30,4 +30,5 @@
@import "mixins";
@import "mobile/mixins";
@import "tree/tree";
@import "mobile/tree";
@import "search/search";
@import "mobile/tree";

@ -238,10 +238,12 @@
bottom: $interiorMargin;
}
}
.search-holder {
top: $ueTopBarH + $interiorMarginLg;
}
.tree-holder {
overflow: auto;
top: $ueTopBarH + $interiorMarginLg;
padding-right: $interiorMargin;
top: $ueTopBarH + $interiorMarginLg + $treeSearchInputBarH + $interiorMargin;
}
}
&.items {

@ -37,7 +37,7 @@
class='ui-symbol view-control'
mct-object="domainObject"
ng-model="ngModel"
ng-click="treeNode.checkMobile() ? ngModel.selectedObject = domainObject :
ng-click="treeNode.checkMobile() ? treeNode.setObject(ngModel, domainObject) :
toggle.toggle(); treeNode.trackExpansion()"
ng-if="model.composition !== undefined || treeNode.checkMobile()"
>

@ -21,6 +21,11 @@
*****************************************************************************/
/*global define*/
/**
* This bundle provides various general-purpose UI elements, including
* platform styling.
* @namespace platform/commonUI/general
*/
define(
[],
function () {
@ -29,6 +34,7 @@ define(
/**
* The StyleSheetLoader adds links to style sheets exposed from
* various bundles as extensions of category `stylesheets`.
* @memberof platform/commonUI/general
* @constructor
* @param {object[]} stylesheets stylesheet extension definitions
* @param $document Angular's jqLite-wrapped document element
@ -62,4 +68,4 @@ define(
return StyleSheetLoader;
}
);
);

@ -42,6 +42,7 @@ define(
* * `ungrouped`: All actions which did not have a defined
* group.
*
* @memberof platform/commonUI/general
* @constructor
*/
function ActionGroupController($scope) {
@ -102,4 +103,4 @@ define(
return ActionGroupController;
}
);
);

@ -29,6 +29,7 @@ define(
/**
* Controller for the bottombar template. Exposes
* available indicators (of extension category "indicators")
* @memberof platform/commonUI/general
* @constructor
*/
function BottomBarController(indicators) {
@ -42,20 +43,19 @@ define(
};
}
indicators = indicators.map(present);
return {
/**
* Get all indicators to display.
* @returns {Indicator[]} all indicators
* to display in the bottom bar.
*/
getIndicators: function () {
return indicators;
}
};
this.indicators = indicators.map(present);
}
/**
* Get all indicators to display.
* @returns {Indicator[]} all indicators
* to display in the bottom bar.
* @memberof platform/commonUI/general.BottomBarController#
*/
BottomBarController.prototype.getIndicators = function () {
return this.indicators;
};
return BottomBarController;
}
);
);

@ -31,71 +31,69 @@ define(
* menus) where clicking elsewhere in the document while the toggle
* is in an active state is intended to dismiss the toggle.
*
* @memberof platform/commonUI/general
* @constructor
* @param $scope the scope in which this controller is active
* @param $document the document element, injected by Angular
*/
function ClickAwayController($scope, $document) {
var state = false,
clickaway;
var self = this;
// Track state, but also attach and detach a listener for
// mouseup events on the document.
function deactivate() {
state = false;
$document.off("mouseup", clickaway);
}
function activate() {
state = true;
$document.on("mouseup", clickaway);
}
function changeState() {
if (state) {
deactivate();
} else {
activate();
}
}
this.state = false;
this.$scope = $scope;
this.$document = $document;
// Callback used by the document listener. Deactivates;
// note also $scope.$apply is invoked to indicate that
// the state of this controller has changed.
clickaway = function () {
deactivate();
this.clickaway = function () {
self.deactivate();
$scope.$apply();
return false;
};
return {
/**
* Get the current state of the toggle.
* @return {boolean} true if active
*/
isActive: function () {
return state;
},
/**
* Set a new state for the toggle.
* @return {boolean} true to activate
*/
setState: function (newState) {
if (state !== newState) {
changeState();
}
},
/**
* Toggle the current state; activate if it is inactive,
* deactivate if it is active.
*/
toggle: function () {
changeState();
}
};
}
// Track state, but also attach and detach a listener for
// mouseup events on the document.
ClickAwayController.prototype.deactivate = function () {
this.state = false;
this.$document.off("mouseup", this.clickaway);
};
ClickAwayController.prototype.activate = function () {
this.state = true;
this.$document.on("mouseup", this.clickaway);
};
/**
* Get the current state of the toggle.
* @return {boolean} true if active
*/
ClickAwayController.prototype.isActive =function () {
return this.state;
};
/**
* Set a new state for the toggle.
* @return {boolean} true to activate
*/
ClickAwayController.prototype.setState = function (newState) {
if (this.state !== newState) {
this.toggle();
}
};
/**
* Toggle the current state; activate if it is inactive,
* deactivate if it is active.
*/
ClickAwayController.prototype.toggle = function () {
if (this.state) {
this.deactivate();
} else {
this.activate();
}
};
return ClickAwayController;
}
);
);

@ -33,6 +33,7 @@ define(
* Controller for the context menu. Maintains an up-to-date
* list of applicable actions (those from category "contextual")
*
* @memberof platform/commonUI/general
* @constructor
*/
function ContextMenuController($scope) {
@ -49,4 +50,4 @@ define(
return ContextMenuController;
}
);
);

@ -54,6 +54,7 @@ define(
* parameter it received.) Getter-setter functions are never the
* target of a scope assignment and so avoid this problem.
*
* @memberof platform/commonUI/general
* @constructor
* @param {Scope} $scope the controller's scope
*/
@ -87,4 +88,4 @@ define(
return GetterSetterController;
}
);
);

@ -30,6 +30,7 @@ define(
/**
* Controller for the domain object selector control.
* @memberof platform/commonUI/general
* @constructor
* @param {ObjectService} objectService service from which to
* read domain objects
@ -38,28 +39,17 @@ define(
function SelectorController(objectService, $scope) {
var treeModel = {},
listModel = {},
selectedObjects = [],
rootObject,
previousSelected;
previousSelected,
self = this;
// For watch; look at the user's selection in the tree
function getTreeSelection() {
return treeModel.selectedObject;
}
// Get the value of the field being edited
function getField() {
return $scope.ngModel[$scope.field] || [];
}
// Get the value of the field being edited
function setField(value) {
$scope.ngModel[$scope.field] = value;
}
// Store root object for subsequent exposure to template
function storeRoot(objects) {
rootObject = objects[ROOT_ID];
self.rootObject = objects[ROOT_ID];
}
// Check that a selection is of the valid type
@ -82,7 +72,8 @@ define(
function updateSelectedObjects(objects) {
// Look up from the
function getObject(id) { return objects[id]; }
selectedObjects = ids.filter(getObject).map(getObject);
self.selectedObjects =
ids.filter(getObject).map(getObject);
}
// Look up objects by id, then populate right-hand list
@ -93,64 +84,85 @@ define(
$scope.$watch(getTreeSelection, validateTreeSelection);
// Make sure right-hand list matches underlying model
$scope.$watchCollection(getField, updateList);
$scope.$watchCollection(function () {
return self.getField();
}, updateList);
// Look up root object, then store it
objectService.getObjects([ROOT_ID]).then(storeRoot);
return {
/**
* Get the root object to show in the left-hand tree.
* @returns {DomainObject} the root object
*/
root: function () {
return rootObject;
},
/**
* Add a domain object to the list of selected objects.
* @param {DomainObject} the domain object to select
*/
select: function (domainObject) {
var id = domainObject && domainObject.getId(),
list = getField() || [];
// Only select if we have a valid id,
// and it isn't already selected
if (id && list.indexOf(id) === -1) {
setField(list.concat([id]));
}
},
/**
* Remove a domain object from the list of selected objects.
* @param {DomainObject} the domain object to select
*/
deselect: function (domainObject) {
var id = domainObject && domainObject.getId(),
list = getField() || [];
// Only change if this was a valid id,
// for an object which was already selected
if (id && list.indexOf(id) !== -1) {
// Filter it out of the current field
setField(list.filter(function (otherId) {
return otherId !== id;
}));
// Clear the current list selection
delete listModel.selectedObject;
}
},
/**
* Get the currently-selected domain objects.
* @returns {DomainObject[]} the current selection
*/
selected: function () {
return selectedObjects;
},
// Expose tree/list model for use in template directly
treeModel: treeModel,
listModel: listModel
};
this.$scope = $scope;
this.selectedObjects = [];
// Expose tree/list model for use in template directly
this.treeModel = treeModel;
this.listModel = listModel;
}
// Set the value of the field being edited
SelectorController.prototype.setField = function (value) {
this.$scope.ngModel[this.$scope.field] = value;
};
// Get the value of the field being edited
SelectorController.prototype.getField = function () {
return this.$scope.ngModel[this.$scope.field] || [];
};
/**
* Get the root object to show in the left-hand tree.
* @returns {DomainObject} the root object
*/
SelectorController.prototype.root = function () {
return this.rootObject;
};
/**
* Add a domain object to the list of selected objects.
* @param {DomainObject} the domain object to select
*/
SelectorController.prototype.select = function (domainObject) {
var id = domainObject && domainObject.getId(),
list = this.getField() || [];
// Only select if we have a valid id,
// and it isn't already selected
if (id && list.indexOf(id) === -1) {
this.setField(list.concat([id]));
}
};
/**
* Remove a domain object from the list of selected objects.
* @param {DomainObject} the domain object to select
*/
SelectorController.prototype.deselect = function (domainObject) {
var id = domainObject && domainObject.getId(),
list = this.getField() || [];
// Only change if this was a valid id,
// for an object which was already selected
if (id && list.indexOf(id) !== -1) {
// Filter it out of the current field
this.setField(list.filter(function (otherId) {
return otherId !== id;
}));
// Clear the current list selection
delete this.listModel.selectedObject;
}
};
/**
* Get the currently-selected domain objects.
* @returns {DomainObject[]} the current selection
*/
SelectorController.prototype.selected = function () {
return this.selectedObjects;
};
return SelectorController;
}
);
);

@ -32,59 +32,58 @@ define(
/**
* Controller for the splitter in Browse mode. Current implementation
* uses many hard-coded constants; this could be generalized.
* @memberof platform/commonUI/general
* @constructor
*/
function SplitPaneController() {
var current = 200,
start = 200,
assigned = false;
return {
/**
* Get the current position of the splitter, in pixels
* from the left edge.
* @returns {number} position of the splitter, in pixels
*/
state: function (defaultState) {
// Set the state to the desired default, if we don't have a
// "real" current state yet.
if (arguments.length > 0 && !assigned) {
current = defaultState;
assigned = true;
}
return current;
},
/**
* Begin moving the splitter; this will note the splitter's
* current position, which is necessary for correct
* interpretation of deltas provided by mct-drag.
*/
startMove: function () {
start = current;
},
/**
* Move the splitter a number of pixels to the right
* (negative numbers move the splitter to the left.)
* This movement is relative to the position of the
* splitter when startMove was last invoked.
* @param {number} delta number of pixels to move
*/
move: function (delta, minimum, maximum) {
// Ensure defaults for minimum/maximum
maximum = isNaN(maximum) ? DEFAULT_MAXIMUM : maximum;
minimum = isNaN(minimum) ? DEFAULT_MINIMUM : minimum;
// Update current splitter state
current = Math.min(
maximum,
Math.max(minimum, start + delta)
);
//console.log(current + "; minimum: " + minimum + "; max: " + maximum);
}
};
this.current = 200;
this.start = 200;
this.assigned = false;
}
/**
* Get the current position of the splitter, in pixels
* from the left edge.
* @returns {number} position of the splitter, in pixels
*/
SplitPaneController.prototype.state = function (defaultState) {
// Set the state to the desired default, if we don't have a
// "real" current state yet.
if (arguments.length > 0 && !this.assigned) {
this.current = defaultState;
this.assigned = true;
}
return this.current;
};
/**
* Begin moving the splitter; this will note the splitter's
* current position, which is necessary for correct
* interpretation of deltas provided by mct-drag.
*/
SplitPaneController.prototype.startMove = function () {
this.start = this.current;
};
/**
* Move the splitter a number of pixels to the right
* (negative numbers move the splitter to the left.)
* This movement is relative to the position of the
* splitter when startMove was last invoked.
* @param {number} delta number of pixels to move
*/
SplitPaneController.prototype.move = function (delta, minimum, maximum) {
// Ensure defaults for minimum/maximum
maximum = isNaN(maximum) ? DEFAULT_MAXIMUM : maximum;
minimum = isNaN(minimum) ? DEFAULT_MINIMUM : minimum;
// Update current splitter state
this.current = Math.min(
maximum,
Math.max(minimum, this.start + delta)
);
};
return SplitPaneController;
}
);
);

@ -30,37 +30,37 @@ define(
* A ToggleController is used to activate/deactivate things.
* A common usage is for "twistie"
*
* @memberof platform/commonUI/general
* @constructor
*/
function ToggleController() {
var state = false;
return {
/**
* Get the current state of the toggle.
* @return {boolean} true if active
*/
isActive: function () {
return state;
},
/**
* Set a new state for the toggle.
* @return {boolean} true to activate
*/
setState: function (newState) {
state = newState;
},
/**
* Toggle the current state; activate if it is inactive,
* deactivate if it is active.
*/
toggle: function () {
state = !state;
}
};
this.state = false;
}
/**
* Get the current state of the toggle.
* @return {boolean} true if active
*/
ToggleController.prototype.isActive = function () {
return this.state;
};
/**
* Set a new state for the toggle.
* @return {boolean} true to activate
*/
ToggleController.prototype.setState = function (newState) {
this.state = newState;
};
/**
* Toggle the current state; activate if it is inactive,
* deactivate if it is active.
*/
ToggleController.prototype.toggle = function () {
this.state = !this.state;
};
return ToggleController;
}
);
);

@ -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,Promise*/
/*global define,Promise,window*/
/**
* Module defining TreeNodeController. Created by vwoeltje on 11/10/14.
@ -48,10 +48,12 @@ define(
* node expansion when this tree node's _subtree_ will contain
* the navigated object (recursively, this becomes an
* expand-to-show-navigated-object behavior.)
* @memberof platform/commonUI/general
* @constructor
*/
function TreeNodeController($scope, $timeout, agentService) {
var selectedObject = ($scope.ngModel || {}).selectedObject,
var self = this,
selectedObject = ($scope.ngModel || {}).selectedObject,
isSelected = false,
hasBeenExpanded = false;
@ -75,21 +77,6 @@ define(
((navPath[index] === nodePath[index]) &&
checkPath(nodePath, navPath, index + 1));
}
// Track that a node has been expanded, either by the
// user or automatically to show a selection.
function trackExpansion() {
if (!hasBeenExpanded) {
// Run on a timeout; if a lot of expansion needs to
// occur (e.g. if the selection is several nodes deep) we
// want this to be spread across multiple digest cycles.
$timeout(function () { hasBeenExpanded = true; }, 0);
}
}
function checkMobile() {
return agentService.isMobile(navigator.userAgent);
}
// Consider the currently-navigated object and update
// parameters which support display.
@ -105,7 +92,7 @@ define(
// Deselect; we will reselect below, iff we are
// exactly at the end of the path.
isSelected = false;
self.isSelectedFlag = false;
// Expand if necessary (if the navigated object will
// be in this node's subtree)
@ -124,12 +111,12 @@ define(
// at the end of the path, highlight;
// otherwise, expand.
if (nodePath.length === navPath.length) {
isSelected = true;
self.isSelectedFlag = true;
} else { // node path is shorter: Expand!
if ($scope.toggle) {
$scope.toggle.setState(true);
}
trackExpansion();
self.trackExpansion();
}
}
@ -142,41 +129,69 @@ define(
selectedObject = object;
checkSelection();
}
this.isSelectedFlag = false;
this.hasBeenExpandedFlag = false;
this.$timeout = $timeout;
this.agentService = agentService;
this.$scope = $scope;
// Listen for changes which will effect display parameters
$scope.$watch("ngModel.selectedObject", setSelection);
$scope.$watch("domainObject", checkSelection);
return {
/**
* This method should be called when a node is expanded
* to record that this has occurred, to support one-time
* lazy loading of the node's subtree.
*/
trackExpansion: trackExpansion,
checkMobile: checkMobile,
/**
* Check if this not has ever been expanded.
* @returns true if it has been expanded
*/
hasBeenExpanded: function () {
return hasBeenExpanded;
},
/**
* Check whether or not the domain object represented by
* this tree node should be highlighted.
* An object will be highlighted if it matches
* ngModel.selectedObject
* @returns true if this should be highlighted
*/
isSelected: function () {
return isSelected;
}
};
}
/**
* This method should be called when a node is expanded
* to record that this has occurred, to support one-time
* lazy loading of the node's subtree.
*/
TreeNodeController.prototype.trackExpansion = function () {
var self = this;
if (!self.hasBeenExpanded()) {
// Run on a timeout; if a lot of expansion needs to
// occur (e.g. if the selection is several nodes deep) we
// want this to be spread across multiple digest cycles.
self.$timeout(function () {
self.hasBeenExpandedFlag = true;
}, 0);
}
};
/**
* Check if this not has ever been expanded.
* @returns true if it has been expanded
*/
TreeNodeController.prototype.hasBeenExpanded = function () {
return this.hasBeenExpandedFlag;
};
/**
* Check whether or not the domain object represented by
* this tree node should be highlighted.
* An object will be highlighted if it matches
* ngModel.selectedObject
* @returns true if this should be highlighted
*/
TreeNodeController.prototype.isSelected = function () {
return this.isSelectedFlag;
};
// Sets the selected object in the tree, to be the
// currently represented object. If the user is on phone
// and in portrait mode, than, hide the tree menu
TreeNodeController.prototype.setObject = function (ngModel, domainObject) {
ngModel.selectedObject = domainObject;
if (this.agentService.getOrientation(window.innerWidth, window.innerHeight) === "portrait" &&
this.agentService.isPhone(navigator.userAgent)) {
this.$scope.$emit('select-obj');
}
};
TreeNodeController.prototype.checkMobile = function () {
return this.agentService.isMobile(navigator.userAgent);
};
return TreeNodeController;
}
);
);

@ -32,6 +32,7 @@ define(
/**
* Controller for the view switcher; populates and maintains a list
* of applicable views for a represented domain object.
* @memberof platform/commonUI/general
* @constructor
*/
function ViewSwitcherController($scope, $timeout) {
@ -71,3 +72,4 @@ define(
return ViewSwitcherController;
}
);

@ -39,6 +39,7 @@ define(
* plain string attribute, instead of as an Angular
* expression.
*
* @memberof platform/commonUI/general
* @constructor
*/
function MCTContainer(containers) {
@ -96,4 +97,4 @@ define(
return MCTContainer;
}
);
);

@ -44,6 +44,7 @@ define(
* and vertical pixel offset of the current mouse position
* relative to the mouse position where dragging began.
*
* @memberof platform/commonUI/general
* @constructor
*
*/
@ -157,3 +158,4 @@ define(
return MCTDrag;
}
);

@ -49,6 +49,7 @@ define(
* This is an Angular expression, and it will be re-evaluated after
* each interval.
*
* @memberof platform/commonUI/general
* @constructor
*
*/
@ -111,4 +112,4 @@ define(
return MCTResize;
}
);
);

@ -37,6 +37,7 @@ define(
* This is exposed as two directives in `bundle.json`; the difference
* is handled purely by parameterization.
*
* @memberof platform/commonUI/general
* @constructor
* @param $parse Angular's $parse
* @param {string} property property to manage within the HTML element
@ -80,4 +81,4 @@ define(
return MCTScroll;
}
);
);

@ -91,6 +91,7 @@ define(
* etc. can be set on that element to control the splitter's
* allowable positions.
*
* @memberof platform/commonUI/general
* @constructor
*/
function MCTSplitPane($parse, $log) {
@ -213,3 +214,4 @@ define(
}
);

@ -39,6 +39,7 @@ define(
/**
* Implements `mct-splitter` directive.
* @memberof platform/commonUI/general
* @constructor
*/
function MCTSplitter() {
@ -88,3 +89,4 @@ define(
}
);

@ -35,7 +35,7 @@ define(
* info using a comparison between the userAgent and key
* device names
*/
function AgentService($window) {
function AgentService() {
// Gets the UA name if it is one of the following.
// If it is not (a desktop for example) nothing is
@ -67,11 +67,12 @@ define(
}
// Returns the orientation of the device based on the
// device's window dimensions
function getOrientation() {
if ($window.outerWidth > $window.outerHeight) {
// device's window dimensions, pass in the innerWidth
// and innerheight of the current window
function getOrientation(innerWidth, innerHeight) {
if (innerWidth > innerHeight) {
return "landscape";
} else if ($window.outerWidth < $window.outerHeight) {
} else if (innerWidth < innerHeight) {
return "portrait";
}
}

@ -32,62 +32,56 @@ define(
/**
* The url service handles calls for url paths
* using domain objects.
* @constructor
* @memberof platform/commonUI/general
*/
function UrlService($location) {
// Returns the url for the mode wanted
// and the domainObject passed in. A path
// is returned. The view is defaulted to
// the current location's (current object's)
// view set.
function urlForLocation(mode, domainObject) {
var context = domainObject &&
domainObject.getCapability('context'),
objectPath = context ? context.getPath() : [],
ids = objectPath.map(function (domainObject) {
return domainObject.getId();
}),
// Parses the path together. Starts with the
// default index.html file, then the mode passed
// into the service, followed by ids in the url
// joined by '/', and lastly the view path from
// the current location
path = mode + "/" + ids.slice(1).join("/");
return path;
}
// Uses the Url for the current location
// from the urlForLocation function and
// includes the view and the index path
function urlForNewTab(mode, domainObject) {
var viewPath = "?view=" + $location.search().view,
newTabPath =
"index.html#" + urlForLocation(mode, domainObject) + viewPath;
return newTabPath;
}
return {
/**
* Returns the Url path for a specific domain object
* without the index.html path and the view path
* @param {value} value of the browse or edit mode
* for the path
* @param {DomainObject} value of the domain object
* to get the path of
*/
urlForNewTab: urlForNewTab,
/**
* Returns the Url path for a specific domain object
* including the index.html path and the view path
* allowing a new tab to hold the correct characteristics
* @param {value} value of the browse or edit mode
* for the path
* @param {DomainObject} value of the domain object
* to get the path of
*/
urlForLocation: urlForLocation
};
this.$location = $location;
}
/**
* Returns the Url path for a specific domain object
* without the index.html path and the view path
* @param {string} mode value of browse or edit mode
* for the path
* @param {DomainObject} value of the domain object
* to get the path of
* @returns {string} URL for the domain object
*/
UrlService.prototype.urlForLocation = function (mode, domainObject) {
var context = domainObject &&
domainObject.getCapability('context'),
objectPath = context ? context.getPath() : [],
ids = objectPath.map(function (domainObject) {
return domainObject.getId();
});
// Parses the path together. Starts with the
// default index.html file, then the mode passed
// into the service, followed by ids in the url
// joined by '/', and lastly the view path from
// the current location
return mode + "/" + ids.slice(1).join("/");
};
/**
* Returns the Url path for a specific domain object
* including the index.html path and the view path
* allowing a new tab to hold the correct characteristics
* @param {string} mode value of browse or edit mode
* for the path
* @param {DomainObject} value of the domain object
* to get the path of
* @returns {string} URL for the domain object
*/
UrlService.prototype.urlForNewTab = function (mode, domainObject) {
var viewPath = "?view=" + this.$location.search().view,
newTabPath =
"index.html#" + this.urlForLocation(mode, domainObject) +
viewPath;
return newTabPath;
};
return UrlService;
}
);
);

@ -30,6 +30,8 @@ define(
var mockScope,
mockTimeout,
mockAgentService,
mockNgModel,
mockDomainObject,
controller;
function TestObject(id, context) {
@ -42,9 +44,18 @@ define(
}
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", ["$watch", "$on"]);
mockScope = jasmine.createSpyObj("$scope", ["$watch", "$on", "$emit"]);
mockTimeout = jasmine.createSpy("$timeout");
mockAgentService = jasmine.createSpyObj("agentService", ["isMobile"]);
mockAgentService = jasmine.createSpyObj("agentService", ["isMobile", "isPhone", "getOrientation"]);
mockNgModel = jasmine.createSpyObj("ngModel", ["selectedObject"]);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[ "getId", "getCapability", "getModel", "useCapability" ]
);
mockAgentService.getOrientation.andReturn("portrait");
mockAgentService.isPhone.andReturn(true);
controller = new TreeNodeController(mockScope, mockTimeout, mockAgentService);
});
@ -191,6 +202,11 @@ define(
controller.checkMobile();
}
});
it("allows a set object to emit select-obj", function () {
controller.setObject(mockNgModel, mockDomainObject);
expect(mockScope.$emit).toHaveBeenCalledWith('select-obj');
});
});
}
);

@ -38,8 +38,8 @@ define(
// Creates a mockLocation, used to
// do the view search
mockWindow = jasmine.createSpyObj(
"$window",
[ "outerWidth", "outerHeight" ]
"window",
[ "innerWidth", "innerHeight" ]
);
mockNavigator = jasmine.createSpyObj(
@ -47,7 +47,7 @@ define(
[ "userAgent" ]
);
agentService = new AgentService(mockWindow);
agentService = new AgentService();
});
it("get current device user agent", function () {
@ -61,13 +61,8 @@ define(
});
it("get orientation of the current device", function () {
mockWindow.outerWidth = 768;
mockWindow.outerHeight = 1024;
agentService.getOrientation();
mockWindow.outerWidth = 1024;
mockWindow.outerHeight = 768;
agentService.getOrientation();
agentService.getOrientation(1024, 768);
agentService.getOrientation(768, 1024);
});
});
}

@ -20,6 +20,13 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
/**
* This bundle provides support for object inspection (specifically, metadata
* show in bubbles on hover.)
* @namespace platform/commonUI/inspect
*/
define({
BUBBLE_TEMPLATE: "<mct-container key=\"bubble\" " +
"bubble-title=\"{{bubbleTitle}}\" " +
@ -33,4 +40,4 @@ define({
// Max width and margins allowed for bubbles; defined in /platform/commonUI/general/res/sass/_constants.scss
BUBBLE_MARGIN_LR: 10,
BUBBLE_MAX_WIDTH: 300
});
});

@ -30,98 +30,115 @@ define(
* The `info` gesture displays domain object metadata in a
* bubble on hover.
*
* @memberof platform/commonUI/inspect
* @constructor
* @implements {Gesture}
* @param $timeout Angular's `$timeout`
* @param {InfoService} infoService a service which shows info bubbles
* @param {number} DELAY delay, in milliseconds, before bubble appears
* @param {number} delay delay, in milliseconds, before bubble appears
* @param element jqLite-wrapped DOM element
* @param {DomainObject} domainObject the domain object for which to
* show information
*/
function InfoGesture($timeout, agentService, infoService, DELAY, element, domainObject) {
var dismissBubble,
pendingBubble,
mousePosition,
scopeOff;
function InfoGesture($timeout, agentService, infoService, delay, element, domainObject) {
var self = this;
function trackPosition(event) {
// Record mouse position, so bubble can be shown at latest
// mouse position (not just where the mouse entered)
mousePosition = [ event.clientX, event.clientY ];
}
// Callback functions to preserve the "this" pointer (in the
// absence of Function.prototype.bind)
this.showBubbleCallback = function (event) {
self.showBubble(event);
};
this.hideBubbleCallback = function (event) {
self.hideBubble(event);
};
this.trackPositionCallback = function (event) {
self.trackPosition(event);
};
function hideBubble() {
// If a bubble is showing, dismiss it
if (dismissBubble) {
dismissBubble();
element.off('mouseleave', hideBubble);
dismissBubble = undefined;
}
// If a bubble will be shown on a timeout, cancel that
if (pendingBubble) {
$timeout.cancel(pendingBubble);
element.off('mousemove', trackPosition);
element.off('mouseleave', hideBubble);
pendingBubble = undefined;
}
// Also clear mouse position so we don't have a ton of tiny
// arrays allocated while user mouses over things
mousePosition = undefined;
}
// Also make sure we dismiss bubble if representation is destroyed
// before the mouse actually leaves it
this.scopeOff = element.scope().$on('$destroy', this.hideBubbleCallback);
function showBubble(event) {
trackPosition(event);
// Also need to track position during hover
element.on('mousemove', trackPosition);
// Show the bubble, after a suitable delay (if mouse has
// left before this time is up, this will be canceled.)
pendingBubble = $timeout(function () {
dismissBubble = infoService.display(
"info-table",
domainObject.getModel().name,
domainObject.useCapability('metadata'),
mousePosition
);
element.off('mousemove', trackPosition);
pendingBubble = undefined;
}, DELAY);
element.on('mouseleave', hideBubble);
}
this.element = element;
this.$timeout = $timeout;
this.infoService = infoService;
this.delay = delay;
this.domainObject = domainObject;
// Checks if you are on a mobile device, if the device is
// not mobile (agentService.isMobile() = false), then
// the pendingBubble and therefore hovering is allowed
if (!agentService.isMobile(navigator.userAgent)) {
// Show bubble (on a timeout) on mouse over
element.on('mouseenter', showBubble);
element.on('mouseenter', this.showBubbleCallback);
}
// Also make sure we dismiss bubble if representation is destroyed
// before the mouse actually leaves it
scopeOff = element.scope().$on('$destroy', hideBubble);
return {
/**
* Detach any event handlers associated with this gesture.
* @memberof InfoGesture
* @method
*/
destroy: function () {
// Dismiss any active bubble...
hideBubble();
// ...and detach listeners
element.off('mouseenter', showBubble);
scopeOff();
}
};
}
InfoGesture.prototype.trackPosition = function (event) {
// Record mouse position, so bubble can be shown at latest
// mouse position (not just where the mouse entered)
this.mousePosition = [ event.clientX, event.clientY ];
};
InfoGesture.prototype.hideBubble = function () {
// If a bubble is showing, dismiss it
if (this.dismissBubble) {
this.dismissBubble();
this.element.off('mouseleave', this.hideBubbleCallback);
this.dismissBubble = undefined;
}
// If a bubble will be shown on a timeout, cancel that
if (this.pendingBubble) {
this.$timeout.cancel(this.pendingBubble);
this.element.off('mousemove', this.trackPositionCallback);
this.element.off('mouseleave', this.hideBubbleCallback);
this.pendingBubble = undefined;
}
// Also clear mouse position so we don't have a ton of tiny
// arrays allocated while user mouses over things
this.mousePosition = undefined;
};
InfoGesture.prototype.showBubble = function (event) {
var self = this;
this.trackPosition(event);
// Also need to track position during hover
this.element.on('mousemove', this.trackPositionCallback);
// Show the bubble, after a suitable delay (if mouse has
// left before this time is up, this will be canceled.)
this.pendingBubble = this.$timeout(function () {
self.dismissBubble = self.infoService.display(
"info-table",
self.domainObject.getModel().name,
self.domainObject.useCapability('metadata'),
self.mousePosition
);
self.element.off('mousemove', self.trackPositionCallback);
self.pendingBubble = undefined;
}, this.delay);
this.element.on('mouseleave', this.hideBubbleCallback);
};
/**
* Detach any event handlers associated with this gesture.
* @method
*/
InfoGesture.prototype.destroy = function () {
// Dismiss any active bubble...
this.hideBubble();
// ...and detach listeners
this.element.off('mouseenter', this.showBubbleCallback);
this.scopeOff();
};
return InfoGesture;
}
);

@ -31,76 +31,79 @@ define(
/**
* Displays informative content ("info bubbles") for the user.
* @memberof platform/commonUI/inspect
* @constructor
*/
function InfoService($compile, $document, $window, $rootScope, agentService) {
this.$compile = $compile;
this.$document = $document;
this.$window = $window;
this.$rootScope = $rootScope;
this.agentService = agentService;
}
function display(templateKey, title, content, position) {
var body = $document.find('body'),
scope = $rootScope.$new(),
winDim = [$window.innerWidth, $window.innerHeight],
bubbleSpaceLR = InfoConstants.BUBBLE_MARGIN_LR + InfoConstants.BUBBLE_MAX_WIDTH,
goLeft = position[0] > (winDim[0] - bubbleSpaceLR),
goUp = position[1] > (winDim[1] / 2),
bubble;
/**
* Display an info bubble at the specified location.
* @param {string} templateKey template to place in bubble
* @param {string} title title for the bubble
* @param {*} content content to pass to the template, via
* `ng-model`
* @param {number[]} x,y position of the info bubble, in
* pixel coordinates.
* @returns {Function} a function that may be invoked to
* dismiss the info bubble
*/
InfoService.prototype.display = function (templateKey, title, content, position) {
var $compile = this.$compile,
$document = this.$document,
$window = this.$window,
$rootScope = this.$rootScope,
body = $document.find('body'),
scope = $rootScope.$new(),
winDim = [$window.innerWidth, $window.innerHeight],
bubbleSpaceLR = InfoConstants.BUBBLE_MARGIN_LR + InfoConstants.BUBBLE_MAX_WIDTH,
goLeft = position[0] > (winDim[0] - bubbleSpaceLR),
goUp = position[1] > (winDim[1] / 2),
bubble;
// Pass model & container parameters into the scope
scope.bubbleModel = content;
scope.bubbleTemplate = templateKey;
scope.bubbleLayout = (goUp ? 'arw-btm' : 'arw-top') + ' ' +
(goLeft ? 'arw-right' : 'arw-left');
scope.bubbleTitle = title;
// Pass model & container parameters into the scope
scope.bubbleModel = content;
scope.bubbleTemplate = templateKey;
scope.bubbleLayout = (goUp ? 'arw-btm' : 'arw-top') + ' ' +
(goLeft ? 'arw-right' : 'arw-left');
scope.bubbleTitle = title;
// Create the context menu
bubble = $compile(BUBBLE_TEMPLATE)(scope);
// Create the context menu
bubble = $compile(BUBBLE_TEMPLATE)(scope);
// Position the bubble:
// Phone: On a phone the bubble is specifically positioned
// so that it takes up the width of the screen.
// Tablet/Desktop: On other devices with larger screens, the
// info bubble positioned as normal (with triangle pointing
// to where clicked or pressed)
bubble.css('position', 'absolute');
if (agentService.isPhone(navigator.userAgent)) {
bubble.css('right', '0px');
bubble.css('left', '0px');
bubble.css('top', 'auto');
bubble.css('bottom', '25px');
// Position the bubble
bubble.css('position', 'absolute');
if (this.agentService.isPhone(navigator.userAgent)) {
bubble.css('right', '0px');
bubble.css('left', '0px');
bubble.css('top', 'auto');
bubble.css('bottom', '25px');
} else {
if (goLeft) {
bubble.css('right', (winDim[0] - position[0] + OFFSET[0]) + 'px');
} else {
if (goLeft) {
bubble.css('right', (winDim[0] - position[0] + OFFSET[0]) + 'px');
} else {
bubble.css('left', position[0] + OFFSET[0] + 'px');
}
if (goUp) {
bubble.css('bottom', (winDim[1] - position[1] + OFFSET[1]) + 'px');
} else {
bubble.css('top', position[1] + OFFSET[1] + 'px');
}
bubble.css('left', position[0] + OFFSET[0] + 'px');
}
if (goUp) {
bubble.css('bottom', (winDim[1] - position[1] + OFFSET[1]) + 'px');
} else {
bubble.css('top', position[1] + OFFSET[1] + 'px');
}
// Add the menu to the body
body.append(bubble);
// Return a function to dismiss the bubble
return function () { bubble.remove(); };
}
return {
/**
* Display an info bubble at the specified location.
* @param {string} templateKey template to place in bubble
* @param {string} title title for the bubble
* @param {*} content content to pass to the template, via
* `ng-model`
* @param {number[]} x,y position of the info bubble, in
* pixel coordinates.
* @returns {Function} a function that may be invoked to
* dismiss the info bubble
*/
display: display
};
}
// Add the menu to the body
body.append(bubble);
// Return a function to dismiss the bubble
return function () { bubble.remove(); };
};
return InfoService;
}
);

@ -32,9 +32,11 @@ define(
* 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 table = {};
var self = this;
// Build an initial model for a type
function buildModel(type) {
@ -52,25 +54,26 @@ define(
function addToTable(type) {
var typeKey = type.getKey();
Object.keys(getCapabilities(type)).forEach(function (key) {
table[key] = table[key] || {};
table[key][typeKey] = true;
self.table[key] = self.table[key] || {};
self.table[key][typeKey] = true;
});
}
// Build the table
this.table = {};
(typeService.listTypes() || []).forEach(addToTable);
return {
/**
* Check if a type is expected to expose a specific
* capability.
*/
hasCapability: function (typeKey, capabilityKey) {
return (table[capabilityKey] || {})[typeKey];
}
};
}
/**
* 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;
}
);
);

@ -34,47 +34,51 @@ define(
* since it's delegated to a different policy category.
* To avoid a circular dependency, the service is obtained via
* Angular's `$injector`.
* @constructor
* @memberof platform/containment
* @implements {Policy.<Action, ActionContext>}
*/
function ComposeActionPolicy($injector) {
var policyService;
function allowComposition(containerObject, selectedObject) {
// Get the object types involved in the compose action
var containerType = containerObject &&
containerObject.getCapability('type'),
selectedType = selectedObject &&
selectedObject.getCapability('type');
// Get a reference to the policy service if needed...
policyService = policyService || $injector.get('policyService');
// ...and delegate to the composition policy
return policyService.allow(
'composition',
containerType,
selectedType
);
}
return {
/**
* Check whether or not a compose action should be allowed
* in this context.
* @returns {boolean} true if it may be allowed
*/
allow: function (candidate, context) {
if (candidate.getMetadata().key === 'compose') {
return allowComposition(
(context || {}).domainObject,
(context || {}).selectedObject
);
}
return true;
}
this.getPolicyService = function () {
return $injector.get('policyService');
};
}
ComposeActionPolicy.prototype.allowComposition = function (containerObject, selectedObject) {
// Get the object types involved in the compose action
var containerType = containerObject &&
containerObject.getCapability('type'),
selectedType = selectedObject &&
selectedObject.getCapability('type');
// Get a reference to the policy service if needed...
this.policyService = this.policyService || this.getPolicyService();
// ...and delegate to the composition policy
return this.policyService.allow(
'composition',
containerType,
selectedType
);
};
/**
* Check whether or not a compose action should be allowed
* in this context.
* @returns {boolean} true if it may be allowed
* @memberof platform/containment.ComposeActionPolicy#
*/
ComposeActionPolicy.prototype.allow = function (candidate, context) {
if (candidate.getMetadata().key === 'compose') {
return this.allowComposition(
(context || {}).domainObject,
(context || {}).selectedObject
);
}
return true;
};
return ComposeActionPolicy;
}
);
);

@ -8,21 +8,19 @@ define(
/**
* Policy allowing composition only for domain object types which
* have a composition property.
* @constructor
* @memberof platform/containment
* @implements {Policy.<Type, Type>}
*/
function CompositionModelPolicy() {
return {
/**
* Is the type identified by the candidate allowed to
* contain the type described by the context?
*/
allow: function (candidate, context) {
return Array.isArray(
(candidate.getInitialModel() || {}).composition
);
}
};
}
CompositionModelPolicy.prototype.allow = function (candidate, context) {
return Array.isArray(
(candidate.getInitialModel() || {}).composition
);
};
return CompositionModelPolicy;
}
);
);

@ -28,24 +28,20 @@ define(
/**
* Disallow composition changes to objects which are not mutable.
* @memberof platform/containment
* @constructor
* @implements {Policy.<Type, Type>}
*/
function CompositionMutabilityPolicy() {
return {
/**
* Is the type identified by the candidate allowed to
* contain the type described by the context?
* @param {Type} candidate the type of domain object
*/
allow: function (candidate) {
// Equate creatability with mutability; that is, users
// can only modify objects of types they can create, and
// vice versa.
return candidate.hasFeature('creation');
}
};
}
CompositionMutabilityPolicy.prototype.allow = function (candidate) {
// Equate creatability with mutability; that is, users
// can only modify objects of types they can create, and
// vice versa.
return candidate.hasFeature('creation');
};
return CompositionMutabilityPolicy;
}
);
);

@ -21,6 +21,11 @@
*****************************************************************************/
/*global define*/
/**
* This bundle implements "containment" rules, which determine which objects
* can be contained within which other objects.
* @namespace platform/containment
*/
define(
['./ContainmentTable'],
function (ContainmentTable) {
@ -28,30 +33,27 @@ define(
/**
* Defines composition policy as driven by type metadata.
* @constructor
* @memberof platform/containment
* @implements {Policy.<Type, Type>}
*/
function CompositionPolicy($injector) {
// We're really just wrapping the containment table and rephrasing
// it as a policy decision.
var table;
function getTable() {
this.getTable = function () {
return (table = table || new ContainmentTable(
$injector.get('typeService'),
$injector.get('capabilityService')
));
}
return {
/**
* Is the type identified by the candidate allowed to
* contain the type described by the context?
*/
allow: function (candidate, context) {
return getTable().canContain(candidate, context);
}
};
}
CompositionPolicy.prototype.allow = function (candidate, context) {
return this.getTable().canContain(candidate, context);
};
return CompositionPolicy;
}
);
);

@ -37,15 +37,13 @@ define(
* 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 types = typeService.listTypes(),
capabilityTable = new CapabilityTable(typeService, capabilityService),
table = {};
// Check if one type can contain another
function canContain(containerType, containedType) {
}
var self = this,
types = typeService.listTypes(),
capabilityTable = new CapabilityTable(typeService, capabilityService);
// Add types which have all these capabilities to the set
// of allowed types
@ -82,38 +80,39 @@ define(
// Check for defined containment restrictions
if (contains === undefined) {
// If not, accept anything
table[key] = ANY;
self.table[key] = ANY;
} else {
// Start with an empty set...
table[key] = {};
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(table[key], c);
addToSet(self.table[key], c);
});
}
}
// Build the table
this.table = {};
types.forEach(addToTable);
return {
/**
* Check if domain objects of one type can contain domain
* objects of another type.
* @returns {boolean} true if allowable
*/
canContain: function (containerType, containedType) {
var set = 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()];
}
};
}
/**
* 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;
}
);
);

@ -25,51 +25,102 @@ define(
function () {
"use strict";
/**
* Actions are reusable processes/behaviors performed by users within
* the system, typically upon domain objects. Actions are commonly
* exposed to users as menu items or buttons.
*
* Actions are usually registered via the `actions` extension
* category, or (in advanced cases) via an `actionService`
* implementation.
*
* @interface Action
*/
/**
* Perform the behavior associated with this action. The return type
* may vary depending on which action has been performed; in general,
* no return value should be expected.
*
* @method Action#perform
*/
/**
* Get metadata associated with this action.
*
* @method Action#getMetadata
* @returns {ActionMetadata}
*/
/**
* Metadata associated with an Action. Actions of specific types may
* extend this with additional properties.
*
* @typedef {Object} ActionMetadata
* @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} glyph character to display as icon
* @property {ActionContext} context the context in which the action
* will be performed.
*/
/**
* Provides actions that can be performed within specific contexts.
*
* @interface ActionService
*/
/**
* Get actions which can be performed within a certain context.
*
* @method ActionService#getActions
* @param {ActionContext} context the context in which the action will
* be performed
* @return {Action[]} relevant actions
*/
/**
* A description of the context in which an action may occur.
*
* @typedef ActionContext
* @property {DomainObject} [domainObject] the domain object being
* acted upon.
* @property {DomainObject} [selectedObject] the selection at the
* time of action (e.g. the dragged object in a
* drag-and-drop operation.)
* @property {string} [key] the machine-readable identifier of
* the relevant action
* @property {string} [category] a string identifying the category
* of action being performed
*/
/**
* The ActionAggregator makes several actionService
* instances act as those they were one. When requesting
* actions for a given context, results from all
* services will be assembled and concatenated.
*
* @memberof platform/core
* @constructor
* @param {ActionProvider[]} actionProviders an array
* @implements {ActionService}
* @param {ActionService[]} actionProviders an array
* of action services
*/
function ActionAggregator(actionProviders) {
function getActions(context) {
// Get all actions from all providers, reduce down
// to one array by concatenation
return actionProviders.map(function (provider) {
return provider.getActions(context);
}).reduce(function (a, b) {
return a.concat(b);
}, []);
}
return {
/**
* Get a list of actions which are valid in a given
* context.
*
* @param {ActionContext} the context in which
* the action will occur; this is a
* JavaScript object containing key-value
* pairs. Typically, this will contain a
* field "domainObject" which refers to
* the domain object that will be acted
* upon, but may contain arbitrary information
* recognized by specific providers.
* @return {Action[]} an array of actions which
* may be performed in the provided context.
*
* @method
* @memberof ActionAggregator
*/
getActions: getActions
};
this.actionProviders = actionProviders;
}
ActionAggregator.prototype.getActions = function (context) {
// Get all actions from all providers, reduce down
// to one array by concatenation
return this.actionProviders.map(function (provider) {
return provider.getActions(context);
}).reduce(function (a, b) {
return a.concat(b);
}, []);
};
return ActionAggregator;
}
);
);

@ -45,73 +45,74 @@ define(
* which the action will be performed (also, the
* action which exposes the capability.)
*
* @memberof platform/core
* @constructor
*/
function ActionCapability($q, actionService, domainObject) {
this.$q = $q;
this.actionService = actionService;
this.domainObject = domainObject;
}
/**
* Perform an action. This will find and perform the
* first matching action available for the specified
* context or key.
*
* @param {ActionContext|string} context the context in which
* to perform the action; this is passed along to
* the action service to match against available
* actions. The "domainObject" field will automatically
* be populated with the domain object that exposed
* this capability. If given as a string, this will
* be taken as the "key" field to match against
* specific actions.
* @returns {Promise} the result of the action that was
* performed, or undefined if no matching action
* was found.
* @memberof platform/core.ActionCapability#
*/
ActionCapability.prototype.getActions = function (context) {
// Get all actions which are valid in this context;
// this simply redirects to the action service,
// but additionally adds a domainObject field.
function getActions(context) {
var baseContext = typeof context === 'string' ?
{ key: context } :
(context || {}),
actionContext = Object.create(baseContext);
var baseContext = typeof context === 'string' ?
{ key: context } : (context || {}),
actionContext = Object.create(baseContext);
actionContext.domainObject = domainObject;
actionContext.domainObject = this.domainObject;
return actionService.getActions(actionContext);
}
return this.actionService.getActions(actionContext);
};
/**
* Get actions which are available for this domain object,
* in this context.
*
* @param {ActionContext|string} context the context in which
* to perform the action; this is passed along to
* the action service to match against available
* actions. The "domainObject" field will automatically
* be populated with the domain object that exposed
* this capability. If given as a string, this will
* be taken as the "key" field to match against
* specific actions.
* @returns {Action[]} an array of matching actions
* @memberof platform/core.ActionCapability#
*/
ActionCapability.prototype.perform = function (context) {
// Alias to getActions(context)[0].perform, with a
// check for empty arrays.
function performAction(context) {
var actions = getActions(context);
var actions = this.getActions(context);
return $q.when(
(actions && actions.length > 0) ?
actions[0].perform() :
undefined
);
}
return this.$q.when(
(actions && actions.length > 0) ?
actions[0].perform() :
undefined
);
};
return {
/**
* Perform an action. This will find and perform the
* first matching action available for the specified
* context or key.
*
* @param {ActionContext|string} context the context in which
* to perform the action; this is passed along to
* the action service to match against available
* actions. The "domainObject" field will automatically
* be populated with the domain object that exposed
* this capability. If given as a string, this will
* be taken as the "key" field to match against
* specific actions.
* @returns {Promise} the result of the action that was
* performed, or undefined if no matching action
* was found.
*/
perform: performAction,
/**
* Get actions which are available for this domain object,
* in this context.
*
* @param {ActionContext|string} context the context in which
* to perform the action; this is passed along to
* the action service to match against available
* actions. The "domainObject" field will automatically
* be populated with the domain object that exposed
* this capability. If given as a string, this will
* be taken as the "key" field to match against
* specific actions.
* @returns {Action[]} an array of matching actions
*/
getActions: getActions
};
}
return ActionCapability;
}
);
);

@ -35,11 +35,46 @@ define(
* of actions exposed via extension (specifically, the "actions"
* category of extension.)
*
* @memberof platform/core
* @imeplements {ActionService}
* @constructor
*/
function ActionProvider(actions) {
var actionsByKey = {},
actionsByCategory = {};
var self = this;
// Build up look-up tables
this.actions = actions;
this.actionsByKey = {};
this.actionsByCategory = {};
actions.forEach(function (Action) {
// Get an action's category or categories
var categories = Action.category || [];
// Convert to an array if necessary
categories = Array.isArray(categories) ?
categories : [categories];
// Store action under all relevant categories
categories.forEach(function (category) {
self.actionsByCategory[category] =
self.actionsByCategory[category] || [];
self.actionsByCategory[category].push(Action);
});
// Store action by ekey as well
if (Action.key) {
self.actionsByKey[Action.key] =
self.actionsByKey[Action.key] || [];
self.actionsByKey[Action.key].push(Action);
}
});
}
ActionProvider.prototype.getActions = function (actionContext) {
var context = (actionContext || {}),
category = context.category,
key = context.key,
candidates;
// Instantiate an action; invokes the constructor and
// additionally fills in the action's getMetadata method
@ -70,86 +105,32 @@ define(
function createIfApplicable(actions, context) {
return (actions || []).filter(function (Action) {
return Action.appliesTo ?
Action.appliesTo(context) : true;
Action.appliesTo(context) : true;
}).map(function (Action) {
return instantiateAction(Action, context);
});
}
// Get an array of actions that are valid in the supplied context.
function getActions(actionContext) {
var context = (actionContext || {}),
category = context.category,
key = context.key,
candidates;
// Match actions to the provided context by comparing "key"
// and/or "category" parameters, if specified.
candidates = actions;
if (key) {
candidates = actionsByKey[key];
if (category) {
candidates = candidates.filter(function (Action) {
return Action.category === category;
});
}
} else if (category) {
candidates = actionsByCategory[category];
// Match actions to the provided context by comparing "key"
// and/or "category" parameters, if specified.
candidates = this.actions;
if (key) {
candidates = this.actionsByKey[key];
if (category) {
candidates = candidates.filter(function (Action) {
return Action.category === category;
});
}
// Instantiate those remaining actions, with additional
// filtering per any appliesTo methods defined on those
// actions.
return createIfApplicable(candidates, context);
} else if (category) {
candidates = this.actionsByCategory[category];
}
// Build up look-up tables
actions.forEach(function (Action) {
// Get an action's category or categories
var categories = Action.category || [];
// Convert to an array if necessary
categories = Array.isArray(categories) ?
categories : [categories];
// Store action under all relevant categories
categories.forEach(function (category) {
actionsByCategory[category] =
actionsByCategory[category] || [];
actionsByCategory[category].push(Action);
});
// Store action by ekey as well
if (Action.key) {
actionsByKey[Action.key] =
actionsByKey[Action.key] || [];
actionsByKey[Action.key].push(Action);
}
});
return {
/**
* Get a list of actions which are valid in a given
* context.
*
* @param {ActionContext} the context in which
* the action will occur; this is a
* JavaScript object containing key-value
* pairs. Typically, this will contain a
* field "domainObject" which refers to
* the domain object that will be acted
* upon, but may contain arbitrary information
* recognized by specific providers.
* @return {Action[]} an array of actions which
* may be performed in the provided context.
*
* @method
* @memberof ActionProvider
*/
getActions: getActions
};
}
// Instantiate those remaining actions, with additional
// filtering per any appliesTo methods defined on those
// actions.
return createIfApplicable(candidates, context);
};
return ActionProvider;
}
);
);

@ -34,9 +34,21 @@ define(
* the actions it exposes always emit a log message when they are
* performed.
*
* @memberof platform/core
* @constructor
* @implements {ActionService}
* @param $log Angular's logging service
* @param {ActionService} actionService the decorated action service
*/
function LoggingActionDecorator($log, actionService) {
this.$log = $log;
this.actionService = actionService;
}
LoggingActionDecorator.prototype.getActions = function () {
var actionService = this.actionService,
$log = this.$log;
// Decorate the perform method of the specified action, such that
// it emits a log message whenever performed.
function addLogging(action) {
@ -58,35 +70,12 @@ define(
return logAction;
}
return {
/**
* Get a list of actions which are valid in a given
* context. These actions will additionally log
* themselves when performed.
*
* @param {ActionContext} the context in which
* the action will occur; this is a
* JavaScript object containing key-value
* pairs. Typically, this will contain a
* field "domainObject" which refers to
* the domain object that will be acted
* upon, but may contain arbitrary information
* recognized by specific providers.
* @return {Action[]} an array of actions which
* may be performed in the provided context.
*
* @method
* @memberof LoggingActionDecorator
*/
getActions: function () {
return actionService.getActions.apply(
actionService,
arguments
).map(addLogging);
}
};
}
return actionService.getActions.apply(
actionService,
arguments
).map(addLogging);
};
return LoggingActionDecorator;
}
);
);

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