mirror of
https://github.com/nasa/openmct.git
synced 2025-02-21 01:42:31 +00:00
Merge branch 'open684' into open615
Merge in toolbar support for use in user interface for Fixed Position view, WTD-615.
This commit is contained in:
commit
61c3472266
@ -1,4 +1,7 @@
|
||||
<div ng-controller="ExampleFormController">
|
||||
<mct-toolbar structure="toolbar" ng-model="state" name="aToolbar">
|
||||
</mct-toolbar>
|
||||
|
||||
<mct-form structure="form" ng-model="state" name="aForm">
|
||||
</mct-form>
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
/*global define*/
|
||||
/*global define,window*/
|
||||
|
||||
define(
|
||||
[],
|
||||
@ -10,6 +10,89 @@ define(
|
||||
|
||||
};
|
||||
|
||||
$scope.toolbar = {
|
||||
name: "An example toolbar.",
|
||||
sections: [
|
||||
{
|
||||
description: "First section",
|
||||
items: [
|
||||
{
|
||||
name: "X",
|
||||
description: "X coordinate",
|
||||
control: "textfield",
|
||||
pattern: "^\\d+$",
|
||||
disabled: true,
|
||||
size: 2,
|
||||
key: "x"
|
||||
},
|
||||
{
|
||||
name: "Y",
|
||||
description: "Y coordinate",
|
||||
control: "textfield",
|
||||
pattern: "^\\d+$",
|
||||
size: 2,
|
||||
key: "y"
|
||||
},
|
||||
{
|
||||
name: "W",
|
||||
description: "Cell width",
|
||||
control: "textfield",
|
||||
pattern: "^\\d+$",
|
||||
size: 2,
|
||||
key: "w"
|
||||
},
|
||||
{
|
||||
name: "H",
|
||||
description: "Cell height",
|
||||
control: "textfield",
|
||||
pattern: "^\\d+$",
|
||||
size: 2,
|
||||
key: "h"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
description: "Second section",
|
||||
items: [
|
||||
{
|
||||
control: "button",
|
||||
glyph: "1",
|
||||
description: "Button A",
|
||||
click: function () {
|
||||
window.alert("A");
|
||||
}
|
||||
},
|
||||
{
|
||||
control: "button",
|
||||
glyph: "2",
|
||||
description: "Button B",
|
||||
click: function () {
|
||||
window.alert("B");
|
||||
}
|
||||
},
|
||||
{
|
||||
control: "button",
|
||||
glyph: "3",
|
||||
description: "Button C",
|
||||
disabled: true,
|
||||
click: function () {
|
||||
window.alert("C");
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
control: "color",
|
||||
key: "color"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
$scope.form = {
|
||||
name: "An example form.",
|
||||
sections: [
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
<div class='object-holder abs vscroll'>
|
||||
<mct-representation key="representation.selected.key"
|
||||
mct-object="domainObject">
|
||||
mct-object="representation.selected.key && domainObject">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</span>
|
@ -73,7 +73,7 @@ define(
|
||||
// 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;
|
||||
key = (representation || {}).key;
|
||||
// Track the represented object
|
||||
domainObject = representedObject;
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
<ul class="tree">
|
||||
<li ng-if="!composition">
|
||||
<span class="tree-item">
|
||||
<span class="icon wait-spinner"></span>
|
||||
<span class="title-label">Loading...</span>
|
||||
</span>
|
||||
</li>
|
||||
<li ng-repeat="child in composition">
|
||||
<mct-representation key="'tree-node'"
|
||||
mct-object="child"
|
||||
|
@ -20,16 +20,30 @@ define(
|
||||
* models should be retrieved.
|
||||
*/
|
||||
function PersistedModelProvider(persistenceService, $q, SPACE) {
|
||||
// Load a single object model from persistence
|
||||
function loadModel(id) {
|
||||
return persistenceService.readObject(SPACE, id);
|
||||
}
|
||||
|
||||
// Promise all persisted models (in id->model form)
|
||||
function promiseModels(ids) {
|
||||
return $q.all(ids.map(function (id) {
|
||||
return persistenceService.readObject(SPACE, id);
|
||||
})).then(function (models) {
|
||||
// Package the result as id->model
|
||||
function packageResult(models) {
|
||||
var result = {};
|
||||
ids.forEach(function (id, index) {
|
||||
result[id] = models[index];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Filter out "namespaced" identifiers; these are
|
||||
// not expected to be found in database. See WTD-659.
|
||||
ids = ids.filter(function (id) {
|
||||
return id.indexOf(":") === -1;
|
||||
});
|
||||
|
||||
// Give a promise for all persistence lookups...
|
||||
return $q.all(ids.map(loadModel)).then(packageResult);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -81,15 +81,32 @@ define(
|
||||
return capabilities.map(hasCapability).reduce(and, true);
|
||||
}
|
||||
|
||||
// Check if a view and domain object type can be paired;
|
||||
// both can restrict the others they accept.
|
||||
function viewMatchesType(view, type) {
|
||||
var views = type && (type.getDefinition() || {}).views,
|
||||
matches = true;
|
||||
|
||||
// View is restricted to a certain type
|
||||
if (view.type) {
|
||||
matches = matches && type && type.instanceOf(view.type);
|
||||
}
|
||||
|
||||
// Type wishes to restrict its specific views
|
||||
if (Array.isArray(views)) {
|
||||
matches = matches && (views.indexOf(view.key) > -1);
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function getViews(domainObject) {
|
||||
var type = domainObject.useCapability("type");
|
||||
|
||||
// First, filter views by type (matched to domain object type.)
|
||||
// Second, filter by matching capabilities.
|
||||
return views.filter(function (view) {
|
||||
return (!view.type) || type.instanceOf(view.type);
|
||||
}).filter(function (view) {
|
||||
return capabilitiesMatch(
|
||||
return viewMatchesType(view, type) && capabilitiesMatch(
|
||||
domainObject,
|
||||
view.needs || [],
|
||||
view.delegation || false
|
||||
|
@ -87,6 +87,57 @@ define(
|
||||
expect(mockLog.warn).toHaveBeenCalledWith(jasmine.any(String));
|
||||
});
|
||||
|
||||
it("restricts typed views to matching types", function () {
|
||||
var testType = "testType",
|
||||
testView = { key: "x", type: testType },
|
||||
provider = new ViewProvider([testView], mockLog);
|
||||
|
||||
// Include a "type" capability
|
||||
capabilities.type = jasmine.createSpyObj(
|
||||
"type",
|
||||
["instanceOf", "invoke", "getDefinition"]
|
||||
);
|
||||
capabilities.type.invoke.andReturn(capabilities.type);
|
||||
|
||||
// Should be included when types match
|
||||
capabilities.type.instanceOf.andReturn(true);
|
||||
expect(provider.getViews(mockDomainObject))
|
||||
.toEqual([testView]);
|
||||
expect(capabilities.type.instanceOf)
|
||||
.toHaveBeenCalledWith(testType);
|
||||
|
||||
// ...but not when they don't
|
||||
capabilities.type.instanceOf.andReturn(false);
|
||||
expect(provider.getViews(mockDomainObject))
|
||||
.toEqual([]);
|
||||
|
||||
});
|
||||
|
||||
it("enforces view restrictions from types", function () {
|
||||
var testType = "testType",
|
||||
testView = { key: "x" },
|
||||
provider = new ViewProvider([testView], mockLog);
|
||||
|
||||
// Include a "type" capability
|
||||
capabilities.type = jasmine.createSpyObj(
|
||||
"type",
|
||||
["instanceOf", "invoke", "getDefinition"]
|
||||
);
|
||||
capabilities.type.invoke.andReturn(capabilities.type);
|
||||
|
||||
// Should be included when view keys match
|
||||
capabilities.type.getDefinition
|
||||
.andReturn({ views: [testView.key]});
|
||||
expect(provider.getViews(mockDomainObject))
|
||||
.toEqual([testView]);
|
||||
|
||||
// ...but not when they don't
|
||||
capabilities.type.getDefinition
|
||||
.andReturn({ views: ["somethingElse"]});
|
||||
expect(provider.getViews(mockDomainObject))
|
||||
.toEqual([]);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -12,6 +12,11 @@ directive, e.g.:
|
||||
<mct-form ng-model="myModel" structure="myStructure" name="myForm">
|
||||
</mct-form>
|
||||
|
||||
Using toolbars is similar:
|
||||
|
||||
<mct-toolbar ng-model="myModel" structure="myStructure" name="myToolbar">
|
||||
</mct-toolbar>
|
||||
|
||||
The attributes utilized by this form are as follows:
|
||||
|
||||
* `ng-model`: The object which should contain the full form input. Individual
|
||||
@ -55,6 +60,44 @@ Note that `pattern` may be specified as a string, to simplify storing
|
||||
for structures as JSON when necessary. The string should be given in
|
||||
a form appropriate to pass to a `RegExp` constructor.
|
||||
|
||||
## Toolbar structure
|
||||
|
||||
A toolbar's structure is described similarly to forms, except that there
|
||||
is no notion of rows; instead, there are `items`.
|
||||
|
||||
{
|
||||
"name": ... title to display for the form, as a string ...,
|
||||
"sections": [
|
||||
{
|
||||
"name": ... title to display for the section ...,
|
||||
"items": [
|
||||
{
|
||||
"name": ... title to display for this row ...,
|
||||
"control": ... symbolic key for the control ...,
|
||||
"key": ... field name in ng-model ...
|
||||
"pattern": ... optional, reg exp to match against ...
|
||||
"required": ... optional boolean ...
|
||||
"options": [
|
||||
"name": ... name to display (e.g. in a select) ...,
|
||||
"value": ... value to store in the model ...
|
||||
],
|
||||
"disabled": ... true if control should be disabled ...
|
||||
"size": ... size of the control (for textfields) ...
|
||||
"click": ... function to invoke (for buttons) ...
|
||||
"glyph": ... glyph to display (for buttons) ...
|
||||
"text": ... text withiin control (for buttons) ...
|
||||
},
|
||||
... and other rows ...
|
||||
]
|
||||
},
|
||||
... and other sections ...
|
||||
]
|
||||
}
|
||||
|
||||
Note that `pattern` may be specified as a string, to simplify storing
|
||||
for structures as JSON when necessary. The string should be given in
|
||||
a form appropriate to pass to a `RegExp` constructor.
|
||||
|
||||
## Adding controls
|
||||
|
||||
Four standard control types are included in the forms bundle:
|
||||
@ -62,6 +105,8 @@ Four standard control types are included in the forms bundle:
|
||||
* `textfield`: An area to enter plain text.
|
||||
* `select`: A drop-down list of options.
|
||||
* `checkbox`: A box which may be checked/unchecked.
|
||||
* `color`: A color picker.
|
||||
* `button`: A button.
|
||||
* `datetime`: An input for UTC date/time entry; gives result as a
|
||||
UNIX timestamp, in milliseconds since start of 1970, UTC.
|
||||
|
||||
|
@ -7,6 +7,10 @@
|
||||
"key": "mctForm",
|
||||
"implementation": "MCTForm.js"
|
||||
},
|
||||
{
|
||||
"key": "mctToolbar",
|
||||
"implementation": "MCTToolbar.js"
|
||||
},
|
||||
{
|
||||
"key": "mctControl",
|
||||
"implementation": "MCTControl.js",
|
||||
@ -30,6 +34,14 @@
|
||||
"key": "textfield",
|
||||
"templateUrl": "templates/controls/textfield.html"
|
||||
},
|
||||
{
|
||||
"key": "button",
|
||||
"templateUrl": "templates/controls/button.html"
|
||||
},
|
||||
{
|
||||
"key": "color",
|
||||
"templateUrl": "templates/controls/color.html"
|
||||
},
|
||||
{
|
||||
"key": "composite",
|
||||
"templateUrl": "templates/controls/composite.html"
|
||||
@ -45,36 +57,6 @@
|
||||
"key": "CompositeController",
|
||||
"implementation": "controllers/CompositeController.js"
|
||||
}
|
||||
],
|
||||
"templates": [
|
||||
{
|
||||
"key": "_checkbox",
|
||||
"templateUrl": "templates/_checkbox.html"
|
||||
},
|
||||
{
|
||||
"key": "_checkboxes",
|
||||
"templateUrl": "templates/_checkboxes.html"
|
||||
},
|
||||
{
|
||||
"key": "_datetime",
|
||||
"templateUrl": "templates/_datetime.html"
|
||||
},
|
||||
{
|
||||
"key": "_select",
|
||||
"templateUrl": "templates/_select.html"
|
||||
},
|
||||
{
|
||||
"key": "_selects",
|
||||
"templateUrl": "templates/_selects.html"
|
||||
},
|
||||
{
|
||||
"key": "_textfield",
|
||||
"templateUrl": "templates/_textfield.html"
|
||||
},
|
||||
{
|
||||
"key": "_textfields",
|
||||
"templateUrl": "templates/_textfields.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
11
platform/forms/res/templates/controls/button.html
Normal file
11
platform/forms/res/templates/controls/button.html
Normal file
@ -0,0 +1,11 @@
|
||||
<a href=""
|
||||
class="t-btn l-btn s-btn s-icon-btn s-very-subtle"
|
||||
ng-class="{ labeled: structure.text }"
|
||||
ng-click="structure.click()">
|
||||
<span class="ui-symbol icon">
|
||||
{{structure.glyph}}
|
||||
</span>
|
||||
<span class="title-label" ng-if="structure.text">
|
||||
{{structure.text}}
|
||||
</span>
|
||||
</a>
|
3
platform/forms/res/templates/controls/color.html
Normal file
3
platform/forms/res/templates/controls/color.html
Normal file
@ -0,0 +1,3 @@
|
||||
<input type="color"
|
||||
name="mctControl"
|
||||
ng-model="ngModel[field]">
|
@ -4,6 +4,7 @@
|
||||
ng-required="ngRequired"
|
||||
ng-model="ngModel[field]"
|
||||
ng-pattern="ngPattern"
|
||||
size="{{structure.size}}"
|
||||
name="mctControl">
|
||||
</span>
|
||||
</span>
|
||||
|
32
platform/forms/res/templates/toolbar.html
Normal file
32
platform/forms/res/templates/toolbar.html
Normal file
@ -0,0 +1,32 @@
|
||||
<form name="tool-bar btn-bar contents" novalidate>
|
||||
|
||||
<div class="form">
|
||||
<span ng-repeat="section in structure.sections"
|
||||
class="control-group coordinates"
|
||||
title="{{section.description}}">
|
||||
|
||||
<ng-form ng-repeat="item in section.items"
|
||||
ng-class="{ 'input-labeled': item.name }"
|
||||
class="inline"
|
||||
title="{{item.description}}"
|
||||
name="mctFormInner">
|
||||
|
||||
<label ng-if="item.name">
|
||||
{{item.name}}:
|
||||
</label>
|
||||
<mct-control key="item.control"
|
||||
ng-class="{ disabled: item.disabled }"
|
||||
ng-model="ngModel"
|
||||
ng-required="item.required"
|
||||
ng-pattern="getRegExp(item.pattern)"
|
||||
options="item.options"
|
||||
structure="item"
|
||||
field="item.key">
|
||||
</mct-control>
|
||||
|
||||
</ng-form>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</form>
|
@ -4,13 +4,10 @@
|
||||
* Module defining MCTForm. Created by vwoeltje on 11/10/14.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
["./controllers/FormController"],
|
||||
function (FormController) {
|
||||
"use strict";
|
||||
|
||||
// Default ng-pattern; any non whitespace
|
||||
var NON_WHITESPACE = /\S/;
|
||||
|
||||
/**
|
||||
* The mct-form directive allows generation of displayable
|
||||
* forms based on a declarative description of the form's
|
||||
@ -37,45 +34,6 @@ define(
|
||||
"templates/form.html"
|
||||
].join("/");
|
||||
|
||||
function controller($scope) {
|
||||
var regexps = [];
|
||||
|
||||
// ng-pattern seems to want a RegExp, and not a
|
||||
// string (despite what documentation says) but
|
||||
// we want form structure to be JSON-expressible,
|
||||
// so we make RegExp's from strings as-needed
|
||||
function getRegExp(pattern) {
|
||||
// If undefined, don't apply a pattern
|
||||
if (!pattern) {
|
||||
return NON_WHITESPACE;
|
||||
}
|
||||
|
||||
// Just echo if it's already a regexp
|
||||
if (pattern instanceof RegExp) {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
// Otherwise, assume a string
|
||||
// Cache for easy lookup later (so we don't
|
||||
// creat a new RegExp every digest cycle)
|
||||
if (!regexps[pattern]) {
|
||||
regexps[pattern] = new RegExp(pattern);
|
||||
}
|
||||
|
||||
return regexps[pattern];
|
||||
}
|
||||
|
||||
// Publish the form state under the requested
|
||||
// name in the parent scope
|
||||
$scope.$watch("mctForm", function (mctForm) {
|
||||
if ($scope.name) {
|
||||
$scope.$parent[$scope.name] = mctForm;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.getRegExp = getRegExp;
|
||||
}
|
||||
|
||||
return {
|
||||
// Only show at the element level
|
||||
restrict: "E",
|
||||
@ -83,9 +41,8 @@ define(
|
||||
// Load the forms template
|
||||
templateUrl: templatePath,
|
||||
|
||||
// Use the controller defined above to
|
||||
// populate/respond to changes in scope
|
||||
controller: controller,
|
||||
// Use FormController to populate/respond to changes in scope
|
||||
controller: FormController,
|
||||
|
||||
// Initial an isolate scope
|
||||
scope: {
|
||||
|
64
platform/forms/src/MCTToolbar.js
Normal file
64
platform/forms/src/MCTToolbar.js
Normal file
@ -0,0 +1,64 @@
|
||||
/*global define,Promise*/
|
||||
|
||||
/**
|
||||
* Module defining MCTForm. Created by vwoeltje on 11/10/14.
|
||||
*/
|
||||
define(
|
||||
["./controllers/FormController"],
|
||||
function (FormController) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The mct-toolbar directive allows generation of displayable
|
||||
* forms based on a declarative description of the form's
|
||||
* structure.
|
||||
*
|
||||
* This directive accepts three attributes:
|
||||
*
|
||||
* * `ng-model`: The model for the form; where user input
|
||||
* where be stored.
|
||||
* * `structure`: The declarative structure of the toolbar.
|
||||
* Describes what controls should be shown and where
|
||||
* their values should be read/written in the model.
|
||||
* * `name`: The name under which to expose the form's
|
||||
* dirty/valid state. This is similar to ng-form's use
|
||||
* of name, except this will be made available in the
|
||||
* parent scope.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function MCTForm() {
|
||||
var templatePath = [
|
||||
"platform/forms", //MCTForm.bundle.path,
|
||||
"res", //MCTForm.bundle.resources,
|
||||
"templates/toolbar.html"
|
||||
].join("/");
|
||||
|
||||
return {
|
||||
// Only show at the element level
|
||||
restrict: "E",
|
||||
|
||||
// Load the forms template
|
||||
templateUrl: templatePath,
|
||||
|
||||
// Use FormController to populate/respond to changes in scope
|
||||
controller: FormController,
|
||||
|
||||
// Initial an isolate scope
|
||||
scope: {
|
||||
|
||||
// The model: Where form input will actually go
|
||||
ngModel: "=",
|
||||
|
||||
// Form structure; what sections/rows to show
|
||||
structure: "=",
|
||||
|
||||
// Name under which to publish the form
|
||||
name: "@"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return MCTForm;
|
||||
}
|
||||
);
|
57
platform/forms/src/controllers/FormController.js
Normal file
57
platform/forms/src/controllers/FormController.js
Normal file
@ -0,0 +1,57 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
// Default ng-pattern; any non whitespace
|
||||
var NON_WHITESPACE = /\S/;
|
||||
|
||||
/**
|
||||
* Controller for mct-form and mct-toolbar directives.
|
||||
* @constructor
|
||||
*/
|
||||
function FormController($scope) {
|
||||
var regexps = [];
|
||||
|
||||
// ng-pattern seems to want a RegExp, and not a
|
||||
// string (despite what documentation says) but
|
||||
// we want form structure to be JSON-expressible,
|
||||
// so we make RegExp's from strings as-needed
|
||||
function getRegExp(pattern) {
|
||||
// If undefined, don't apply a pattern
|
||||
if (!pattern) {
|
||||
return NON_WHITESPACE;
|
||||
}
|
||||
|
||||
// Just echo if it's already a regexp
|
||||
if (pattern instanceof RegExp) {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
// Otherwise, assume a string
|
||||
// Cache for easy lookup later (so we don't
|
||||
// creat a new RegExp every digest cycle)
|
||||
if (!regexps[pattern]) {
|
||||
regexps[pattern] = new RegExp(pattern);
|
||||
}
|
||||
|
||||
return regexps[pattern];
|
||||
}
|
||||
|
||||
// Publish the form state under the requested
|
||||
// name in the parent scope
|
||||
$scope.$watch("mctForm", function (mctForm) {
|
||||
if ($scope.name) {
|
||||
$scope.$parent[$scope.name] = mctForm;
|
||||
}
|
||||
});
|
||||
|
||||
// Expose the regexp lookup
|
||||
$scope.getRegExp = getRegExp;
|
||||
}
|
||||
|
||||
return FormController;
|
||||
}
|
||||
);
|
90
platform/forms/test/MCTToolbarSpec.js
Normal file
90
platform/forms/test/MCTToolbarSpec.js
Normal file
@ -0,0 +1,90 @@
|
||||
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/MCTToolbar"],
|
||||
function (MCTToolbar) {
|
||||
"use strict";
|
||||
|
||||
describe("The mct-toolbar directive", function () {
|
||||
var mockScope,
|
||||
mctToolbar;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]);
|
||||
mockScope.$parent = {};
|
||||
mctToolbar = new MCTToolbar();
|
||||
});
|
||||
|
||||
it("is restricted to elements", function () {
|
||||
expect(mctToolbar.restrict).toEqual("E");
|
||||
});
|
||||
|
||||
it("watches for changes in form by name", function () {
|
||||
// mct-form needs to watch for the form by name
|
||||
// in order to convey changes in $valid, $dirty, etc
|
||||
// up to the parent scope.
|
||||
mctToolbar.controller(mockScope);
|
||||
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"mctForm",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("conveys form status to parent scope", function () {
|
||||
var someState = { someKey: "some value" };
|
||||
mockScope.name = "someName";
|
||||
|
||||
mctToolbar.controller(mockScope);
|
||||
|
||||
mockScope.$watch.mostRecentCall.args[1](someState);
|
||||
|
||||
expect(mockScope.$parent.someName).toBe(someState);
|
||||
});
|
||||
|
||||
it("allows strings to be converted to RegExps", function () {
|
||||
// This is needed to support ng-pattern in the template
|
||||
mctToolbar.controller(mockScope);
|
||||
|
||||
// Should have added getRegExp to the scope,
|
||||
// to convert strings to regular expressions
|
||||
expect(mockScope.getRegExp("^\\d+$")).toEqual(/^\d+$/);
|
||||
});
|
||||
|
||||
it("returns the same regexp instance for the same string", function () {
|
||||
// Don't want new instances each digest cycle, for performance
|
||||
var strRegExp = "^[a-z]\\d+$",
|
||||
regExp;
|
||||
|
||||
// Add getRegExp to scope
|
||||
mctToolbar.controller(mockScope);
|
||||
regExp = mockScope.getRegExp(strRegExp);
|
||||
|
||||
// Same object instance each time...
|
||||
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
|
||||
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
|
||||
});
|
||||
|
||||
it("passes RegExp objects through untouched", function () {
|
||||
// Permit using forms to simply provide their own RegExp object
|
||||
var regExp = /^\d+[a-d]$/;
|
||||
|
||||
// Add getRegExp to scope
|
||||
mctToolbar.controller(mockScope);
|
||||
|
||||
// Should have added getRegExp to the scope,
|
||||
// to convert strings to regular expressions
|
||||
expect(mockScope.getRegExp(regExp)).toBe(regExp);
|
||||
});
|
||||
|
||||
it("passes a non-whitespace regexp when no pattern is defined", function () {
|
||||
// If no pattern is supplied, ng-pattern should match anything
|
||||
mctToolbar.controller(mockScope);
|
||||
expect(mockScope.getRegExp()).toEqual(/\S/);
|
||||
expect(mockScope.getRegExp(undefined)).toEqual(/\S/);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
69
platform/forms/test/controllers/FormControllerSpec.js
Normal file
69
platform/forms/test/controllers/FormControllerSpec.js
Normal file
@ -0,0 +1,69 @@
|
||||
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/controllers/FormController"],
|
||||
function (FormController) {
|
||||
"use strict";
|
||||
|
||||
describe("The form controller", function () {
|
||||
var mockScope,
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]);
|
||||
mockScope.$parent = {};
|
||||
controller = new FormController(mockScope);
|
||||
});
|
||||
|
||||
it("watches for changes in form by name", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"mctForm",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("conveys form status to parent scope", function () {
|
||||
var someState = { someKey: "some value" };
|
||||
mockScope.name = "someName";
|
||||
mockScope.$watch.mostRecentCall.args[1](someState);
|
||||
expect(mockScope.$parent.someName).toBe(someState);
|
||||
});
|
||||
|
||||
it("allows strings to be converted to RegExps", function () {
|
||||
// Should have added getRegExp to the scope,
|
||||
// to convert strings to regular expressions
|
||||
expect(mockScope.getRegExp("^\\d+$")).toEqual(/^\d+$/);
|
||||
});
|
||||
|
||||
it("returns the same regexp instance for the same string", function () {
|
||||
// Don't want new instances each digest cycle, for performance
|
||||
var strRegExp = "^[a-z]\\d+$",
|
||||
regExp;
|
||||
|
||||
// Add getRegExp to scope
|
||||
regExp = mockScope.getRegExp(strRegExp);
|
||||
|
||||
// Same object instance each time...
|
||||
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
|
||||
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
|
||||
});
|
||||
|
||||
it("passes RegExp objects through untouched", function () {
|
||||
// Permit using forms to simply provide their own RegExp object
|
||||
var regExp = /^\d+[a-d]$/;
|
||||
|
||||
// Should have added getRegExp to the scope,
|
||||
// to convert strings to regular expressions
|
||||
expect(mockScope.getRegExp(regExp)).toBe(regExp);
|
||||
});
|
||||
|
||||
it("passes a non-whitespace regexp when no pattern is defined", function () {
|
||||
// If no pattern is supplied, ng-pattern should match anything
|
||||
expect(mockScope.getRegExp()).toEqual(/\S/);
|
||||
expect(mockScope.getRegExp(undefined)).toEqual(/\S/);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -2,5 +2,6 @@
|
||||
"MCTControl",
|
||||
"MCTForm",
|
||||
"controllers/CompositeController",
|
||||
"controllers/DateTimeController"
|
||||
"controllers/DateTimeController",
|
||||
"controllers/FormController"
|
||||
]
|
@ -8,6 +8,12 @@
|
||||
"type": "provider",
|
||||
"implementation": "CouchPersistenceProvider.js",
|
||||
"depends": [ "$http", "$q", "PERSISTENCE_SPACE", "COUCHDB_PATH" ]
|
||||
},
|
||||
{
|
||||
"provides": "persistenceService",
|
||||
"type": "decorator",
|
||||
"implementation": "CachingPersistenceDecorator.js",
|
||||
"depends": [ "PERSISTENCE_SPACE" ]
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
|
155
platform/persistence/src/CachingPersistenceDecorator.js
Normal file
155
platform/persistence/src/CachingPersistenceDecorator.js
Normal file
@ -0,0 +1,155 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* A caching persistence decorator maintains local copies of persistent objects
|
||||
* that have been loaded, and keeps them in sync after writes. This allows
|
||||
* retrievals to occur more quickly after the first load.
|
||||
*
|
||||
* @constructor
|
||||
* @param {string[]} CACHE_SPACES persistence space names which
|
||||
* should be cached
|
||||
* @param {PersistenceService} persistenceService the service which
|
||||
* implements object persistence, whose inputs/outputs
|
||||
* should be cached.
|
||||
*/
|
||||
function CachingPersistenceDecorator(CACHE_SPACES, persistenceService) {
|
||||
var spaces = CACHE_SPACES || [], // List of spaces to cache
|
||||
cache = {}; // Where objects will be stored
|
||||
|
||||
// Utility function; avoid sharing one instance everywhere.
|
||||
function clone(value) {
|
||||
// Only clone truthy values (no need to clone undefined, false...)
|
||||
return value && JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
// Place value in the cache for space, if there is one.
|
||||
function addToCache(space, key, value) {
|
||||
if (cache[space]) {
|
||||
cache[space][key] = { value: clone(value) };
|
||||
}
|
||||
}
|
||||
|
||||
// Create a function for putting value into a cache;
|
||||
// useful for then-chaining.
|
||||
function putCache(space, key) {
|
||||
return function (value) {
|
||||
addToCache(space, key, value);
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap as a thenable; used instead of $q.when because that
|
||||
// will resolve on a future tick, which can cause latency
|
||||
// issues (which this decorator is intended to address.)
|
||||
function fastPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return fastPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Arrayify list of spaces to cache, if necessary.
|
||||
spaces = Array.isArray(spaces) ? spaces : [ spaces ];
|
||||
|
||||
// Initialize caches
|
||||
spaces.forEach(function (space) {
|
||||
cache[space] = {};
|
||||
});
|
||||
|
||||
// Provide PersistenceService interface; mostly delegate to the
|
||||
// decorated service, intervene and cache where appropriate.
|
||||
return {
|
||||
/**
|
||||
* List all persistence spaces that are supported by the
|
||||
* decorated service.
|
||||
* @memberof CachingPersistenceDecorator#
|
||||
* @returns {Promise.<string[]>} spaces supported
|
||||
*/
|
||||
listSpaces: function () {
|
||||
return persistenceService.listSpaces();
|
||||
},
|
||||
/**
|
||||
* List all objects in a specific space.
|
||||
* @memberof CachingPersistenceDecorator#
|
||||
* @param {string} space the space in which to list objects
|
||||
* @returns {Promise.<string[]>} keys for objects in this space
|
||||
*/
|
||||
listObjects: function (space) {
|
||||
return persistenceService.listObjects(space);
|
||||
},
|
||||
/**
|
||||
* Create an object in a specific space. This will
|
||||
* be cached to expedite subsequent retrieval.
|
||||
* @memberof CachingPersistenceDecorator#
|
||||
* @param {string} space the space in which to create the object
|
||||
* @param {string} key the key associate with the object for
|
||||
* subsequent lookup
|
||||
* @param {object} value a JSONifiable object to store
|
||||
* @returns {Promise.<boolean>} an indicator of the success or
|
||||
* failure of this request
|
||||
*/
|
||||
createObject: function (space, key, value) {
|
||||
addToCache(space, key, value);
|
||||
return persistenceService.createObject(space, key, value);
|
||||
},
|
||||
/**
|
||||
* Read an object from a specific space. This will read from a
|
||||
* cache if the object is available.
|
||||
* @memberof CachingPersistenceDecorator#
|
||||
* @param {string} space the space in which to create the object
|
||||
* @param {string} key the key which identifies the object
|
||||
* @returns {Promise.<object>} a promise for the object; may
|
||||
* resolve to undefined (if the object does not exist
|
||||
* in this space)
|
||||
*/
|
||||
readObject: function (space, key) {
|
||||
return (cache[space] && cache[space][key]) ?
|
||||
fastPromise(clone(cache[space][key].value)) :
|
||||
persistenceService.readObject(space, key)
|
||||
.then(putCache(space, key));
|
||||
},
|
||||
/**
|
||||
* Update an object in a specific space. This will
|
||||
* be cached to expedite subsequent retrieval.
|
||||
* @memberof CachingPersistenceDecorator#
|
||||
* @param {string} space the space in which to create the object
|
||||
* @param {string} key the key associate with the object for
|
||||
* subsequent lookup
|
||||
* @param {object} value a JSONifiable object to store
|
||||
* @returns {Promise.<boolean>} an indicator of the success or
|
||||
* failure of this request
|
||||
*/
|
||||
updateObject: function (space, key, value) {
|
||||
addToCache(space, key, value);
|
||||
return persistenceService.updateObject(space, key, value);
|
||||
},
|
||||
/**
|
||||
* Delete an object in a specific space. This will
|
||||
* additionally be cleared from the cache.
|
||||
* @memberof CachingPersistenceDecorator#
|
||||
* @param {string} space the space in which to create the object
|
||||
* @param {string} key the key associate with the object for
|
||||
* subsequent lookup
|
||||
* @param {object} value a JSONifiable object to delete
|
||||
* @returns {Promise.<boolean>} an indicator of the success or
|
||||
* failure of this request
|
||||
*/
|
||||
deleteObject: function (space, key, value) {
|
||||
if (cache[space]) {
|
||||
delete cache[space][key];
|
||||
}
|
||||
return persistenceService.deleteObject(space, key, value);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return CachingPersistenceDecorator;
|
||||
}
|
||||
);
|
82
platform/persistence/test/CachingPersistenceDecoratorSpec.js
Normal file
82
platform/persistence/test/CachingPersistenceDecoratorSpec.js
Normal file
@ -0,0 +1,82 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/CachingPersistenceDecorator"],
|
||||
function (CachingPersistenceDecorator) {
|
||||
"use strict";
|
||||
|
||||
var PERSISTENCE_METHODS = [
|
||||
"listSpaces",
|
||||
"listObjects",
|
||||
"createObject",
|
||||
"readObject",
|
||||
"updateObject",
|
||||
"deleteObject"
|
||||
];
|
||||
|
||||
describe("The caching persistence decorator", function () {
|
||||
var testSpace,
|
||||
mockPersistence,
|
||||
mockCallback,
|
||||
decorator;
|
||||
|
||||
function mockPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
|
||||
testSpace = "TEST";
|
||||
mockPersistence = jasmine.createSpyObj(
|
||||
"persistenceService",
|
||||
PERSISTENCE_METHODS
|
||||
);
|
||||
mockCallback = jasmine.createSpy("callback");
|
||||
|
||||
PERSISTENCE_METHODS.forEach(function (m) {
|
||||
mockPersistence[m].andReturn(mockPromise({
|
||||
method: m
|
||||
}));
|
||||
});
|
||||
|
||||
decorator = new CachingPersistenceDecorator(
|
||||
testSpace,
|
||||
mockPersistence
|
||||
);
|
||||
});
|
||||
|
||||
it("delegates all methods", function () {
|
||||
PERSISTENCE_METHODS.forEach(function (m) {
|
||||
// Reset the callback
|
||||
mockCallback = jasmine.createSpy("callback");
|
||||
// Invoke the method; avoid using a key that will be cached
|
||||
decorator[m](testSpace, "testKey" + m, "testValue")
|
||||
.then(mockCallback);
|
||||
// Should have gotten that method's plain response
|
||||
expect(mockCallback).toHaveBeenCalledWith({ method: m });
|
||||
});
|
||||
});
|
||||
|
||||
it("does not repeat reads of cached objects", function () {
|
||||
// Perform two reads
|
||||
decorator.readObject(testSpace, "someKey", "someValue")
|
||||
.then(mockCallback);
|
||||
decorator.readObject(testSpace, "someKey", "someValue")
|
||||
.then(mockCallback);
|
||||
|
||||
// Should have only delegated once
|
||||
expect(mockPersistence.readObject.calls.length).toEqual(1);
|
||||
|
||||
// But both promises should have resolved
|
||||
expect(mockCallback.calls.length).toEqual(2);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -1,4 +1,5 @@
|
||||
[
|
||||
"CachingPersistenceDecorator",
|
||||
"CouchDocument",
|
||||
"CouchIndicator",
|
||||
"CouchPersistenceProvider"
|
||||
|
@ -1,6 +1,16 @@
|
||||
{
|
||||
"name": "Data bundle",
|
||||
"description": "Interfaces and infrastructure for real-time and historical data.",
|
||||
"configuration": {
|
||||
"paths": {
|
||||
"moment": "moment.min.js"
|
||||
},
|
||||
"shim": {
|
||||
"moment": {
|
||||
"exports": "moment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"components": [
|
||||
{
|
||||
|
@ -47,6 +47,9 @@ define(
|
||||
// is being issued.
|
||||
broadcasting: false,
|
||||
|
||||
// Active subscriptions
|
||||
subscriptions: [],
|
||||
|
||||
// Used for getTelemetryObjects; a reference is
|
||||
// stored so that this can be called in a watch
|
||||
telemetryObjects: [],
|
||||
@ -120,6 +123,21 @@ define(
|
||||
}));
|
||||
}
|
||||
|
||||
// Subscribe to streaming telemetry updates
|
||||
function subscribe(telemetryCapability, id) {
|
||||
return telemetryCapability.subscribe(function () {
|
||||
requestTelemetryForId(id, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Stop listening to active subscriptions
|
||||
function unsubscribe() {
|
||||
self.subscriptions.forEach(function (s) {
|
||||
return s && s();
|
||||
});
|
||||
self.subscriptions = [];
|
||||
}
|
||||
|
||||
// Look up domain objects which have telemetry capabilities.
|
||||
// This will either be the object in view, or object that
|
||||
// this object delegates its telemetry capability to.
|
||||
@ -149,12 +167,17 @@ define(
|
||||
function buildResponseContainer(domainObject) {
|
||||
var telemetry = domainObject &&
|
||||
domainObject.getCapability("telemetry"),
|
||||
id,
|
||||
metadata;
|
||||
|
||||
if (telemetry) {
|
||||
id = domainObject.getId();
|
||||
|
||||
self.subscriptions.push(subscribe(telemetry, id));
|
||||
|
||||
metadata = telemetry.getMetadata();
|
||||
|
||||
self.response[domainObject.getId()] = {
|
||||
self.response[id] = {
|
||||
name: domainObject.getModel().name,
|
||||
domainObject: domainObject,
|
||||
metadata: metadata,
|
||||
@ -187,6 +210,9 @@ define(
|
||||
// domain objects, and update some controller-internal
|
||||
// state to support subsequent calls.
|
||||
function buildResponseContainers(domainObjects) {
|
||||
// Clear out any existing subscriptions
|
||||
unsubscribe();
|
||||
|
||||
// Build the containers
|
||||
domainObjects.forEach(buildResponseContainer);
|
||||
|
||||
@ -217,6 +243,7 @@ define(
|
||||
// scope. This will be the domain object itself, or
|
||||
// its telemetry delegates, or both.
|
||||
function getTelemetryObjects(domainObject) {
|
||||
unsubscribe();
|
||||
promiseRelevantDomainObjects(domainObject)
|
||||
.then(buildResponseContainers);
|
||||
}
|
||||
@ -241,6 +268,7 @@ define(
|
||||
|
||||
// Stop polling for changes
|
||||
function deactivate() {
|
||||
unsubscribe();
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ define(
|
||||
}
|
||||
|
||||
function formatRangeValue(v, key) {
|
||||
return isNaN(v) ? "" : v.toFixed(3);
|
||||
return isNaN(v) ? v : v.toFixed(3);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -12,6 +12,7 @@ define(
|
||||
mockLog,
|
||||
mockDomainObject,
|
||||
mockTelemetry,
|
||||
mockUnsubscribe,
|
||||
controller;
|
||||
|
||||
function mockPromise(value) {
|
||||
@ -44,8 +45,9 @@ define(
|
||||
|
||||
mockTelemetry = jasmine.createSpyObj(
|
||||
"telemetry",
|
||||
[ "requestData", "getMetadata" ]
|
||||
[ "requestData", "subscribe", "getMetadata" ]
|
||||
);
|
||||
mockUnsubscribe = jasmine.createSpy("unsubscribe");
|
||||
|
||||
mockQ.when.andCallFake(mockPromise);
|
||||
mockQ.all.andReturn(mockPromise([mockDomainObject]));
|
||||
@ -63,6 +65,7 @@ define(
|
||||
mockTelemetry.requestData.andReturn(mockPromise({
|
||||
telemetryKey: "some value"
|
||||
}));
|
||||
mockTelemetry.subscribe.andReturn(mockUnsubscribe);
|
||||
|
||||
controller = new TelemetryController(
|
||||
mockScope,
|
||||
@ -187,6 +190,16 @@ define(
|
||||
.toHaveBeenCalledWith("telemetryUpdate");
|
||||
});
|
||||
|
||||
it("subscribes for streaming telemetry updates", function () {
|
||||
// Push into scope to create subscriptions
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
// Should have subscribed
|
||||
expect(mockTelemetry.subscribe)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
// Invoke the subscriber function (for coverage)
|
||||
mockTelemetry.subscribe.mostRecentCall.args[0]({});
|
||||
});
|
||||
|
||||
it("listens for scope destruction to clean up", function () {
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
"$destroy",
|
||||
@ -195,6 +208,16 @@ define(
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
});
|
||||
|
||||
it("unsubscribes when destroyed", function () {
|
||||
// Push into scope to create subscriptions
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
// Invoke "$destroy" listener
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
|
||||
// Should have unsubscribed
|
||||
expect(mockUnsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user