Compare commits
53 Commits
api-type-d
...
open1003
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
182c15864c | ||
|
|
b0f06a2195 | ||
|
|
f9c93ca022 | ||
|
|
ee0fa0451a | ||
|
|
e86e955682 | ||
|
|
9913fb48f5 | ||
|
|
71c362f016 | ||
|
|
fa6e8fd5f9 | ||
|
|
23b64951f3 | ||
|
|
99590d18f7 | ||
|
|
86b31bc040 | ||
|
|
d52bfed1df | ||
|
|
44d6456de1 | ||
|
|
beeefe517a | ||
|
|
d02f4041b2 | ||
|
|
9fd75ff91e | ||
|
|
026ece3956 | ||
|
|
1d6880c283 | ||
|
|
8ddad9bf4c | ||
|
|
f167022eea | ||
|
|
b1266abf01 | ||
|
|
5a2d1a746d | ||
|
|
4f0e3fdf85 | ||
|
|
be9f56107c | ||
|
|
787f3815df | ||
|
|
35d7d9b380 | ||
|
|
dc577d4c24 | ||
|
|
8f9308de01 | ||
|
|
8f7a5e113b | ||
|
|
9820f9d9c5 | ||
|
|
56ff98cce7 | ||
|
|
dade6b2254 | ||
|
|
e9cac6eff3 | ||
|
|
5689279954 | ||
|
|
f9fd97230f | ||
|
|
536e2290b8 | ||
|
|
73b922facf | ||
|
|
ba0d9a186b | ||
|
|
80f5cb756d | ||
|
|
d7f566088f | ||
|
|
a3bcaea7f9 | ||
|
|
23c71b7218 | ||
|
|
463f7ccf65 | ||
|
|
87fe407739 | ||
|
|
bb4f1ce7cd | ||
|
|
0cc2ba7595 | ||
|
|
8162429106 | ||
|
|
ed519d89d7 | ||
|
|
0e4f6185b8 | ||
|
|
1ced47fc2c | ||
|
|
f16a107105 | ||
|
|
f683ca44a2 | ||
|
|
546cde56a8 |
@@ -18,7 +18,6 @@
|
||||
"node-uuid": "^1.4.7",
|
||||
"comma-separated-values": "^3.6.4",
|
||||
"FileSaver.js": "^0.0.2",
|
||||
"zepto": "^1.1.6",
|
||||
"eventemitter3": "^1.2.0"
|
||||
"zepto": "^1.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,10 @@
|
||||
<script type="text/javascript">
|
||||
require(['main'], function (mct) {
|
||||
require([
|
||||
'./tutorials/todo/todo',
|
||||
'./tutorials/todo/bundle',
|
||||
'./example/imagery/bundle',
|
||||
'./example/eventGenerator/bundle',
|
||||
'./example/generator/bundle'
|
||||
], function (todoPlugin) {
|
||||
todoPlugin(mct);
|
||||
mct.start();
|
||||
})
|
||||
], mct.run.bind(mct));
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">
|
||||
|
||||
24
main.js
24
main.js
@@ -28,7 +28,6 @@ requirejs.config({
|
||||
"angular-route": "bower_components/angular-route/angular-route.min",
|
||||
"csv": "bower_components/comma-separated-values/csv.min",
|
||||
"es6-promise": "bower_components/es6-promise/promise.min",
|
||||
"EventEmitter": "bower_components/eventemitter3/index",
|
||||
"moment": "bower_components/moment/moment",
|
||||
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
|
||||
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
|
||||
@@ -44,9 +43,6 @@ requirejs.config({
|
||||
"angular-route": {
|
||||
"deps": ["angular"]
|
||||
},
|
||||
"EventEmitter": {
|
||||
"exports": "EventEmitter"
|
||||
},
|
||||
"moment-duration-format": {
|
||||
"deps": ["moment"]
|
||||
},
|
||||
@@ -62,9 +58,6 @@ requirejs.config({
|
||||
define([
|
||||
'./platform/framework/src/Main',
|
||||
'legacyRegistry',
|
||||
'./src/MCT',
|
||||
|
||||
'./src/adapter/bundle',
|
||||
|
||||
'./platform/framework/bundle',
|
||||
'./platform/core/bundle',
|
||||
@@ -100,14 +93,11 @@ define([
|
||||
'./platform/search/bundle',
|
||||
'./platform/status/bundle',
|
||||
'./platform/commonUI/regions/bundle'
|
||||
], function (Main, legacyRegistry, MCT) {
|
||||
var mct = new MCT();
|
||||
|
||||
mct.legacyRegistry = legacyRegistry;
|
||||
mct.run = mct.start;
|
||||
mct.on('start', function () {
|
||||
return new Main().run(legacyRegistry);
|
||||
});
|
||||
|
||||
return mct;
|
||||
], function (Main, legacyRegistry) {
|
||||
return {
|
||||
legacyRegistry: legacyRegistry,
|
||||
run: function () {
|
||||
return new Main().run(legacyRegistry);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "0.10.2-SNAPSHOT",
|
||||
"version": "0.10.3-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"dependencies": {
|
||||
"express": "^4.13.1",
|
||||
|
||||
@@ -27,6 +27,7 @@ define([
|
||||
"./src/MenuArrowController",
|
||||
"./src/navigation/NavigationService",
|
||||
"./src/navigation/NavigateAction",
|
||||
"./src/navigation/OrphanNavigationHandler",
|
||||
"./src/windowing/NewTabAction",
|
||||
"./src/windowing/FullscreenAction",
|
||||
"./src/windowing/WindowTitler",
|
||||
@@ -47,6 +48,7 @@ define([
|
||||
MenuArrowController,
|
||||
NavigationService,
|
||||
NavigateAction,
|
||||
OrphanNavigationHandler,
|
||||
NewTabAction,
|
||||
FullscreenAction,
|
||||
WindowTitler,
|
||||
@@ -253,6 +255,14 @@ define([
|
||||
"$rootScope",
|
||||
"$document"
|
||||
]
|
||||
},
|
||||
{
|
||||
"implementation": OrphanNavigationHandler,
|
||||
"depends": [
|
||||
"throttle",
|
||||
"topic",
|
||||
"navigationService"
|
||||
]
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([], function () {
|
||||
|
||||
/**
|
||||
* Navigates away from orphan objects whenever they are detected.
|
||||
*
|
||||
* An orphan object is an object whose apparent parent does not
|
||||
* actually contain it. This may occur in certain circumstances, such
|
||||
* as when persistence succeeds for a newly-created object but fails
|
||||
* for its parent.
|
||||
*
|
||||
* @param throttle the `throttle` service
|
||||
* @param topic the `topic` service
|
||||
* @param navigationService the `navigationService`
|
||||
* @constructor
|
||||
*/
|
||||
function OrphanNavigationHandler(throttle, topic, navigationService) {
|
||||
var throttledCheckNavigation;
|
||||
|
||||
function getParent(domainObject) {
|
||||
var context = domainObject.getCapability('context');
|
||||
return context.getParent();
|
||||
}
|
||||
|
||||
function isOrphan(domainObject) {
|
||||
var parent = getParent(domainObject),
|
||||
composition = parent.getModel().composition,
|
||||
id = domainObject.getId();
|
||||
return !composition || (composition.indexOf(id) === -1);
|
||||
}
|
||||
|
||||
function navigateToParent(domainObject) {
|
||||
var parent = getParent(domainObject);
|
||||
return parent.getCapability('action').perform('navigate');
|
||||
}
|
||||
|
||||
function checkNavigation() {
|
||||
var navigatedObject = navigationService.getNavigation();
|
||||
if (navigatedObject.hasCapability('context') &&
|
||||
isOrphan(navigatedObject)) {
|
||||
if (!navigatedObject.getCapability('editor').isEditContextRoot()) {
|
||||
navigateToParent(navigatedObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throttledCheckNavigation = throttle(checkNavigation);
|
||||
|
||||
navigationService.addListener(throttledCheckNavigation);
|
||||
topic('mutation').listen(throttledCheckNavigation);
|
||||
}
|
||||
|
||||
return OrphanNavigationHandler;
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'../../src/navigation/OrphanNavigationHandler'
|
||||
], function (OrphanNavigationHandler) {
|
||||
describe("OrphanNavigationHandler", function () {
|
||||
var mockTopic,
|
||||
mockThrottle,
|
||||
mockMutationTopic,
|
||||
mockNavigationService,
|
||||
mockDomainObject,
|
||||
mockParentObject,
|
||||
mockContext,
|
||||
mockActionCapability,
|
||||
mockEditor,
|
||||
testParentModel,
|
||||
testId,
|
||||
mockThrottledFns;
|
||||
|
||||
beforeEach(function () {
|
||||
testId = 'some-identifier';
|
||||
|
||||
mockThrottledFns = [];
|
||||
testParentModel = {};
|
||||
|
||||
mockTopic = jasmine.createSpy('topic');
|
||||
mockThrottle = jasmine.createSpy('throttle');
|
||||
mockNavigationService = jasmine.createSpyObj('navigationService', [
|
||||
'getNavigation',
|
||||
'addListener'
|
||||
]);
|
||||
mockMutationTopic = jasmine.createSpyObj('mutationTopic', [
|
||||
'listen'
|
||||
]);
|
||||
mockDomainObject = jasmine.createSpyObj('domainObject', [
|
||||
'getId',
|
||||
'getCapability',
|
||||
'getModel',
|
||||
'hasCapability'
|
||||
]);
|
||||
mockParentObject = jasmine.createSpyObj('domainObject', [
|
||||
'getId',
|
||||
'getCapability',
|
||||
'getModel',
|
||||
'hasCapability'
|
||||
]);
|
||||
mockContext = jasmine.createSpyObj('context', ['getParent']);
|
||||
mockActionCapability = jasmine.createSpyObj('action', ['perform']);
|
||||
mockEditor = jasmine.createSpyObj('editor', ['isEditContextRoot']);
|
||||
|
||||
mockThrottle.andCallFake(function (fn) {
|
||||
var mockThrottledFn =
|
||||
jasmine.createSpy('throttled-' + mockThrottledFns.length);
|
||||
mockThrottledFn.andCallFake(fn);
|
||||
mockThrottledFns.push(mockThrottledFn);
|
||||
return mockThrottledFn;
|
||||
});
|
||||
mockTopic.andCallFake(function (k) {
|
||||
return k === 'mutation' && mockMutationTopic;
|
||||
});
|
||||
mockDomainObject.getId.andReturn(testId);
|
||||
mockDomainObject.getCapability.andCallFake(function (c) {
|
||||
return {
|
||||
context: mockContext,
|
||||
editor: mockEditor
|
||||
}[c];
|
||||
});
|
||||
mockDomainObject.hasCapability.andCallFake(function (c) {
|
||||
return !!mockDomainObject.getCapability(c);
|
||||
});
|
||||
mockParentObject.getModel.andReturn(testParentModel);
|
||||
mockParentObject.getCapability.andCallFake(function (c) {
|
||||
return {
|
||||
action: mockActionCapability
|
||||
}[c];
|
||||
});
|
||||
mockContext.getParent.andReturn(mockParentObject);
|
||||
mockNavigationService.getNavigation.andReturn(mockDomainObject);
|
||||
mockEditor.isEditContextRoot.andReturn(false);
|
||||
|
||||
return new OrphanNavigationHandler(
|
||||
mockThrottle,
|
||||
mockTopic,
|
||||
mockNavigationService
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("listens for mutation with a throttled function", function () {
|
||||
expect(mockMutationTopic.listen)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
expect(mockThrottledFns.indexOf(
|
||||
mockMutationTopic.listen.mostRecentCall.args[0]
|
||||
)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("listens for navigation changes with a throttled function", function () {
|
||||
expect(mockNavigationService.addListener)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
expect(mockThrottledFns.indexOf(
|
||||
mockNavigationService.addListener.mostRecentCall.args[0]
|
||||
)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
[false, true].forEach(function (isOrphan) {
|
||||
var prefix = isOrphan ? "" : "non-";
|
||||
describe("for " + prefix + "orphan objects", function () {
|
||||
beforeEach(function () {
|
||||
testParentModel.composition = isOrphan ? [] : [testId];
|
||||
});
|
||||
|
||||
[false, true].forEach(function (isEditRoot) {
|
||||
var caseName = isEditRoot ?
|
||||
"that are being edited" : "that are not being edited";
|
||||
|
||||
function itNavigatesAsExpected() {
|
||||
if (isOrphan && !isEditRoot) {
|
||||
it("navigates to the parent", function () {
|
||||
expect(mockActionCapability.perform)
|
||||
.toHaveBeenCalledWith('navigate');
|
||||
});
|
||||
} else {
|
||||
it("does nothing", function () {
|
||||
expect(mockActionCapability.perform)
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe(caseName, function () {
|
||||
beforeEach(function () {
|
||||
mockEditor.isEditContextRoot.andReturn(isEditRoot);
|
||||
});
|
||||
|
||||
describe("when navigation changes", function () {
|
||||
beforeEach(function () {
|
||||
mockNavigationService.addListener.mostRecentCall
|
||||
.args[0](mockDomainObject);
|
||||
});
|
||||
|
||||
itNavigatesAsExpected();
|
||||
});
|
||||
|
||||
describe("when mutation occurs", function () {
|
||||
beforeEach(function () {
|
||||
mockMutationTopic.listen.mostRecentCall
|
||||
.args[0](mockParentObject);
|
||||
});
|
||||
|
||||
itNavigatesAsExpected();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,10 +67,17 @@ define(
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
return self.persistenceCapability.refresh().then(function (result) {
|
||||
if (self.domainObject.getModel().persisted !== undefined) {
|
||||
//Fetch clean model from persistence
|
||||
return self.persistenceCapability.refresh().then(function (result) {
|
||||
self.persistPending = false;
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
self.persistPending = false;
|
||||
return result;
|
||||
});
|
||||
//Model is undefined in persistence, so return undefined.
|
||||
return self.$q.when(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.transactionService.isActive()) {
|
||||
|
||||
@@ -57,6 +57,15 @@ define(
|
||||
);
|
||||
mockPersistence.persist.andReturn(fastPromise());
|
||||
mockPersistence.refresh.andReturn(fastPromise());
|
||||
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[
|
||||
"getModel"
|
||||
]
|
||||
);
|
||||
mockDomainObject.getModel.andReturn({persisted: 1});
|
||||
|
||||
capability = new TransactionalPersistenceCapability(mockQ, mockTransactionService, mockPersistence, mockDomainObject);
|
||||
});
|
||||
|
||||
@@ -78,6 +87,20 @@ define(
|
||||
expect(mockPersistence.refresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("if transaction is active, cancel call is queued that refreshes model when appropriate", function () {
|
||||
mockTransactionService.isActive.andReturn(true);
|
||||
capability.persist();
|
||||
expect(mockTransactionService.addToTransaction).toHaveBeenCalled();
|
||||
|
||||
mockDomainObject.getModel.andReturn({});
|
||||
mockTransactionService.addToTransaction.mostRecentCall.args[1]();
|
||||
expect(mockPersistence.refresh).not.toHaveBeenCalled();
|
||||
|
||||
mockDomainObject.getModel.andReturn({persisted: 1});
|
||||
mockTransactionService.addToTransaction.mostRecentCall.args[1]();
|
||||
expect(mockPersistence.refresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("persist call is only added to transaction once", function () {
|
||||
mockTransactionService.isActive.andReturn(true);
|
||||
capability.persist();
|
||||
|
||||
@@ -288,8 +288,9 @@ body.desktop .pane .mini-tab-icon.toggle-pane {
|
||||
|
||||
.left {
|
||||
padding-right: $interiorMarginLg;
|
||||
.l-back:not(.s-status-editing) {
|
||||
.l-back {
|
||||
margin-right: $interiorMarginLg;
|
||||
&.s-status-editing { display: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,9 @@ define([
|
||||
this.outerMinimumSpan = 1000; // 1 second
|
||||
this.initialDragValue = undefined;
|
||||
this.formatter = formatService.getFormat(defaultFormat);
|
||||
this.formStartChanged = false;
|
||||
this.formEndChanged = false;
|
||||
|
||||
this.cachedStartValue = undefined;
|
||||
this.cachedEndValue = undefined;
|
||||
|
||||
this.$scope.ticks = [];
|
||||
|
||||
@@ -79,9 +80,7 @@ define([
|
||||
'updateOuterEnd',
|
||||
'updateFormat',
|
||||
'validateStart',
|
||||
'validateEnd',
|
||||
'onFormStartChange',
|
||||
'onFormEndChange'
|
||||
'validateEnd'
|
||||
].forEach(function (boundFn) {
|
||||
this[boundFn] = this[boundFn].bind(this);
|
||||
}, this);
|
||||
@@ -91,8 +90,6 @@ define([
|
||||
this.$scope.$watch("ngModel.outer.start", this.updateOuterStart);
|
||||
this.$scope.$watch("ngModel.outer.end", this.updateOuterEnd);
|
||||
this.$scope.$watch("parameters.format", this.updateFormat);
|
||||
this.$scope.$watch("formModel.start", this.onFormStartChange);
|
||||
this.$scope.$watch("formModel.end", this.onFormEndChange);
|
||||
}
|
||||
|
||||
TimeRangeController.prototype.formatTimestamp = function (ts) {
|
||||
@@ -259,35 +256,17 @@ define([
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.updateBoundsFromForm = function () {
|
||||
if (this.formStartChanged) {
|
||||
this.$scope.ngModel.outer.start =
|
||||
if (this.$scope.formModel.start !== this.cachedStartValue) {
|
||||
this.cachedStartValue =
|
||||
this.$scope.ngModel.outer.start =
|
||||
this.$scope.ngModel.inner.start =
|
||||
this.$scope.formModel.start;
|
||||
this.formStartChanged = false;
|
||||
}
|
||||
if (this.formEndChanged) {
|
||||
this.$scope.ngModel.outer.end =
|
||||
if (this.$scope.formModel.end !== this.cachedEndValue) {
|
||||
this.cachedEndValue =
|
||||
this.$scope.ngModel.outer.end =
|
||||
this.$scope.ngModel.inner.end =
|
||||
this.$scope.formModel.end;
|
||||
this.formEndChanged = false;
|
||||
}
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.onFormStartChange = function (
|
||||
newValue,
|
||||
oldValue
|
||||
) {
|
||||
if (!this.formStartChanged && newValue !== oldValue) {
|
||||
this.formStartChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.onFormEndChange = function (
|
||||
newValue,
|
||||
oldValue
|
||||
) {
|
||||
if (!this.formEndChanged && newValue !== oldValue) {
|
||||
this.formEndChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -91,7 +91,12 @@ define([
|
||||
"name": "Export Timeline as CSV",
|
||||
"category": "contextual",
|
||||
"implementation": ExportTimelineAsCSVAction,
|
||||
"depends": ["exportService", "notificationService"]
|
||||
"depends": [
|
||||
"$log",
|
||||
"exportService",
|
||||
"notificationService",
|
||||
"resources[]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
@@ -467,6 +472,7 @@ define([
|
||||
"implementation": TimelineZoomController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$timeout",
|
||||
"TIMELINE_ZOOM_CONFIGURATION"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
<div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x">
|
||||
<mct-include key="'timeline-ticks'"
|
||||
parameters="{
|
||||
fullWidth: timelineController.width(zoomController),
|
||||
fullWidth: zoomController.width(timelineController.end()),
|
||||
start: scroll.x,
|
||||
width: scroll.width,
|
||||
step: zoomController.toPixels(zoomController.zoom()),
|
||||
@@ -141,7 +141,7 @@
|
||||
mct-scroll-x="scroll.x"
|
||||
mct-scroll-y="scroll.y">
|
||||
<div class="l-width-control"
|
||||
ng-style="{ width: timelineController.width(zoomController) + 'px' }">
|
||||
ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
|
||||
<div class="t-swimlane s-swimlane l-swimlane"
|
||||
ng-repeat="swimlane in timelineController.swimlanes()"
|
||||
ng-class="{
|
||||
@@ -197,7 +197,7 @@
|
||||
<div mct-scroll-x="scroll.x"
|
||||
class="t-pane-r-scroll-h-control l-scroll-control s-scroll-control">
|
||||
<div class="l-width-control"
|
||||
ng-style="{ width: timelineController.width(zoomController) + 'px' }">
|
||||
ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,11 +27,15 @@ define([], function () {
|
||||
* in a domain object's composition.
|
||||
* @param {number} index the zero-based index of the composition
|
||||
* element associated with this column
|
||||
* @param idMap an object containing key value pairs, where keys
|
||||
* are domain object identifiers and values are whatever
|
||||
* should appear in CSV output in their place
|
||||
* @constructor
|
||||
* @implements {platform/features/timeline.TimelineCSVColumn}
|
||||
*/
|
||||
function CompositionColumn(index) {
|
||||
function CompositionColumn(index, idMap) {
|
||||
this.index = index;
|
||||
this.idMap = idMap;
|
||||
}
|
||||
|
||||
CompositionColumn.prototype.name = function () {
|
||||
@@ -41,7 +45,9 @@ define([], function () {
|
||||
CompositionColumn.prototype.value = function (domainObject) {
|
||||
var model = domainObject.getModel(),
|
||||
composition = model.composition || [];
|
||||
return (composition[this.index]) || "";
|
||||
|
||||
return composition.length > this.index ?
|
||||
this.idMap[composition[this.index]] : "";
|
||||
};
|
||||
|
||||
return CompositionColumn;
|
||||
|
||||
@@ -27,14 +27,23 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) {
|
||||
*
|
||||
* @param exportService the service used to perform the CSV export
|
||||
* @param notificationService the service used to show notifications
|
||||
* @param {Array} resources an array of `resources` extensions
|
||||
* @param context the Action's context
|
||||
* @implements {Action}
|
||||
* @constructor
|
||||
* @memberof {platform/features/timeline}
|
||||
*/
|
||||
function ExportTimelineAsCSVAction(exportService, notificationService, context) {
|
||||
function ExportTimelineAsCSVAction(
|
||||
$log,
|
||||
exportService,
|
||||
notificationService,
|
||||
resources,
|
||||
context
|
||||
) {
|
||||
this.$log = $log;
|
||||
this.task = new ExportTimelineAsCSVTask(
|
||||
exportService,
|
||||
resources,
|
||||
context.domainObject
|
||||
);
|
||||
this.notificationService = notificationService;
|
||||
@@ -45,13 +54,15 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) {
|
||||
notification = notificationService.notify({
|
||||
title: "Exporting CSV",
|
||||
unknownProgress: true
|
||||
});
|
||||
}),
|
||||
$log = this.$log;
|
||||
|
||||
return this.task.run()
|
||||
.then(function () {
|
||||
notification.dismiss();
|
||||
})
|
||||
.catch(function () {
|
||||
.catch(function (err) {
|
||||
$log.warn(err);
|
||||
notification.dismiss();
|
||||
notificationService.error("Error exporting CSV");
|
||||
});
|
||||
|
||||
@@ -35,11 +35,13 @@ define([
|
||||
* @constructor
|
||||
* @memberof {platform/features/timeline}
|
||||
* @param exportService the service used to export as CSV
|
||||
* @param resources the `resources` extension category
|
||||
* @param {DomainObject} domainObject the timeline being exported
|
||||
*/
|
||||
function ExportTimelineAsCSVTask(exportService, domainObject) {
|
||||
function ExportTimelineAsCSVTask(exportService, resources, domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.exportService = exportService;
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,9 +52,10 @@ define([
|
||||
*/
|
||||
ExportTimelineAsCSVTask.prototype.run = function () {
|
||||
var exportService = this.exportService;
|
||||
var resources = this.resources;
|
||||
|
||||
function doExport(objects) {
|
||||
var exporter = new TimelineColumnizer(objects),
|
||||
var exporter = new TimelineColumnizer(objects, resources),
|
||||
options = { headers: exporter.headers() };
|
||||
return exporter.rows().then(function (rows) {
|
||||
return exportService.exportCSV(rows, options);
|
||||
|
||||
@@ -23,19 +23,23 @@
|
||||
define([], function () {
|
||||
|
||||
/**
|
||||
* A column showing domain object identifiers.
|
||||
* A column showing identifying domain objects.
|
||||
* @constructor
|
||||
* @param idMap an object containing key value pairs, where keys
|
||||
* are domain object identifiers and values are whatever
|
||||
* should appear in CSV output in their place
|
||||
* @implements {platform/features/timeline.TimelineCSVColumn}
|
||||
*/
|
||||
function IdColumn() {
|
||||
function IdColumn(idMap) {
|
||||
this.idMap = idMap;
|
||||
}
|
||||
|
||||
IdColumn.prototype.name = function () {
|
||||
return "Identifier";
|
||||
return "Index";
|
||||
};
|
||||
|
||||
IdColumn.prototype.value = function (domainObject) {
|
||||
return domainObject.getId();
|
||||
return this.idMap[domainObject.getId()];
|
||||
};
|
||||
|
||||
return IdColumn;
|
||||
|
||||
@@ -27,10 +27,14 @@ define([], function () {
|
||||
* @constructor
|
||||
* @param {number} index the zero-based index of the composition
|
||||
* element associated with this column
|
||||
* @param idMap an object containing key value pairs, where keys
|
||||
* are domain object identifiers and values are whatever
|
||||
* should appear in CSV output in their place
|
||||
* @implements {platform/features/timeline.TimelineCSVColumn}
|
||||
*/
|
||||
function ModeColumn(index) {
|
||||
function ModeColumn(index, idMap) {
|
||||
this.index = index;
|
||||
this.idMap = idMap;
|
||||
}
|
||||
|
||||
ModeColumn.prototype.name = function () {
|
||||
@@ -39,8 +43,9 @@ define([], function () {
|
||||
|
||||
ModeColumn.prototype.value = function (domainObject) {
|
||||
var model = domainObject.getModel(),
|
||||
composition = (model.relationships || {}).modes || [];
|
||||
return (composition[this.index]) || "";
|
||||
modes = (model.relationships || {}).modes || [];
|
||||
return modes.length > this.index ?
|
||||
this.idMap[modes[this.index]] : "";
|
||||
};
|
||||
|
||||
return ModeColumn;
|
||||
|
||||
@@ -25,13 +25,15 @@ define([
|
||||
"./ModeColumn",
|
||||
"./CompositionColumn",
|
||||
"./MetadataColumn",
|
||||
"./TimespanColumn"
|
||||
"./TimespanColumn",
|
||||
"./UtilizationColumn"
|
||||
], function (
|
||||
IdColumn,
|
||||
ModeColumn,
|
||||
CompositionColumn,
|
||||
MetadataColumn,
|
||||
TimespanColumn
|
||||
TimespanColumn,
|
||||
UtilizationColumn
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -63,15 +65,17 @@ define([
|
||||
*
|
||||
* @param {DomainObject[]} domainObjects the objects to include
|
||||
* in the exported data
|
||||
* @param {Array} resources an array of `resources` extensions
|
||||
* @constructor
|
||||
* @memberof {platform/features/timeline}
|
||||
*/
|
||||
function TimelineColumnizer(domainObjects) {
|
||||
function TimelineColumnizer(domainObjects, resources) {
|
||||
var maxComposition = 0,
|
||||
maxRelationships = 0,
|
||||
columnNames = {},
|
||||
columns = [],
|
||||
foundTimespan = false,
|
||||
idMap,
|
||||
i;
|
||||
|
||||
function addMetadataProperty(property) {
|
||||
@@ -82,7 +86,12 @@ define([
|
||||
}
|
||||
}
|
||||
|
||||
columns.push(new IdColumn());
|
||||
idMap = domainObjects.reduce(function (map, domainObject, index) {
|
||||
map[domainObject.getId()] = index + 1;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
columns.push(new IdColumn(idMap));
|
||||
|
||||
domainObjects.forEach(function (domainObject) {
|
||||
var model = domainObject.getModel(),
|
||||
@@ -113,12 +122,16 @@ define([
|
||||
columns.push(new TimespanColumn(false));
|
||||
}
|
||||
|
||||
resources.forEach(function (resource) {
|
||||
columns.push(new UtilizationColumn(resource));
|
||||
});
|
||||
|
||||
for (i = 0; i < maxComposition; i += 1) {
|
||||
columns.push(new CompositionColumn(i));
|
||||
columns.push(new CompositionColumn(i, idMap));
|
||||
}
|
||||
|
||||
for (i = 0; i < maxRelationships; i += 1) {
|
||||
columns.push(new ModeColumn(i));
|
||||
columns.push(new ModeColumn(i, idMap));
|
||||
}
|
||||
|
||||
this.domainObjects = domainObjects;
|
||||
|
||||
72
platform/features/timeline/src/actions/UtilizationColumn.js
Normal file
72
platform/features/timeline/src/actions/UtilizationColumn.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2009-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([], function () {
|
||||
/**
|
||||
* A column showing utilization costs associated with activities.
|
||||
* @constructor
|
||||
* @param {string} key the key for the particular cost
|
||||
* @implements {platform/features/timeline.TimelineCSVColumn}
|
||||
*/
|
||||
function UtilizationColumn(resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
UtilizationColumn.prototype.name = function () {
|
||||
var units = {
|
||||
"Kbps": "Kb",
|
||||
"watts": "watt-seconds"
|
||||
}[this.resource.units] || "unknown units";
|
||||
|
||||
return this.resource.name + " (" + units + ")";
|
||||
};
|
||||
|
||||
UtilizationColumn.prototype.value = function (domainObject) {
|
||||
var resource = this.resource;
|
||||
|
||||
function getCost(utilization) {
|
||||
var seconds = (utilization.end - utilization.start) / 1000;
|
||||
return seconds * utilization.value;
|
||||
}
|
||||
|
||||
function getUtilizationValue(utilizations) {
|
||||
utilizations = utilizations.filter(function (utilization) {
|
||||
return utilization.key === resource.key;
|
||||
});
|
||||
|
||||
if (utilizations.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return utilizations.map(getCost).reduce(function (a, b) {
|
||||
return a + b;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return domainObject.hasCapability('utilization') ?
|
||||
domainObject.getCapability('utilization').internal()
|
||||
.then(getUtilizationValue) :
|
||||
"";
|
||||
};
|
||||
|
||||
return UtilizationColumn;
|
||||
});
|
||||
@@ -193,6 +193,13 @@ define(
|
||||
* @returns {Promise.<string[]>} a promise for resource identifiers
|
||||
*/
|
||||
resources: promiseResourceKeys,
|
||||
/**
|
||||
* Get the resource utilization associated with this object
|
||||
* directly, not including any resource utilization associated
|
||||
* with contained objects.
|
||||
* @returns {Promise.<Array>}
|
||||
*/
|
||||
internal: promiseInternalUtilization,
|
||||
/**
|
||||
* Get the resource utilization associated with this
|
||||
* object. Results are not sorted. This requires looking
|
||||
|
||||
@@ -79,15 +79,6 @@ define(
|
||||
graphPopulator.populate(swimlanePopulator.get());
|
||||
}
|
||||
|
||||
// Get pixel width for right pane, using zoom controller
|
||||
function width(zoomController) {
|
||||
var start = swimlanePopulator.start(),
|
||||
end = swimlanePopulator.end();
|
||||
return zoomController.toPixels(zoomController.duration(
|
||||
Math.max(end - start, MINIMUM_DURATION)
|
||||
));
|
||||
}
|
||||
|
||||
// Refresh resource graphs
|
||||
function refresh() {
|
||||
if (graphPopulator) {
|
||||
@@ -121,10 +112,10 @@ define(
|
||||
// Expose active set of swimlanes
|
||||
return {
|
||||
/**
|
||||
* Get the width, in pixels, of the timeline area
|
||||
* @returns {number} width, in pixels
|
||||
* Get the end of the displayed timeline, in milliseconds.
|
||||
* @returns {number} the end of the displayed timeline
|
||||
*/
|
||||
width: width,
|
||||
end: swimlanePopulator.end.bind(swimlanePopulator),
|
||||
/**
|
||||
* Get the swimlanes which should currently be displayed.
|
||||
* @returns {TimelineSwimlane[]} the swimlanes
|
||||
|
||||
@@ -22,27 +22,17 @@
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
var PADDING = 0.25;
|
||||
|
||||
/**
|
||||
* Controls the pan-zoom state of a timeline view.
|
||||
* @constructor
|
||||
*/
|
||||
function TimelineZoomController($scope, ZOOM_CONFIGURATION) {
|
||||
function TimelineZoomController($scope, $timeout, ZOOM_CONFIGURATION) {
|
||||
// Prefer to start with the middle index
|
||||
var zoomLevels = ZOOM_CONFIGURATION.levels || [1000],
|
||||
zoomIndex = Math.floor(zoomLevels.length / 2),
|
||||
tickWidth = ZOOM_CONFIGURATION.width || 200,
|
||||
bounds = { x: 0, width: tickWidth },
|
||||
duration = 86400000; // Default duration in view
|
||||
|
||||
// Round a duration to a larger value, to ensure space for editing
|
||||
function roundDuration(value) {
|
||||
// Ensure there's always an extra day or so
|
||||
var tickCount = bounds.width / tickWidth,
|
||||
sz = zoomLevels[zoomLevels.length - 1] * tickCount;
|
||||
value *= 1.25; // Add 25% padding to start
|
||||
return Math.ceil(value / sz) * sz;
|
||||
}
|
||||
tickWidth = ZOOM_CONFIGURATION.width || 200;
|
||||
|
||||
function toMillis(pixels) {
|
||||
return (pixels / tickWidth) * zoomLevels[zoomIndex];
|
||||
@@ -63,14 +53,20 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
function setScroll(x) {
|
||||
$timeout(function () {
|
||||
$scope.scroll.x = x;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function initializeZoomFromTimespan(timespan) {
|
||||
var timelineDuration = timespan.getDuration();
|
||||
zoomIndex = 0;
|
||||
while (toMillis(bounds.width) < timelineDuration &&
|
||||
while (toMillis($scope.scroll.width) < timelineDuration &&
|
||||
zoomIndex < zoomLevels.length - 1) {
|
||||
zoomIndex += 1;
|
||||
}
|
||||
bounds.x = toPixels(timespan.getStart());
|
||||
setScroll(toPixels(timespan.getStart()));
|
||||
}
|
||||
|
||||
function initializeZoom() {
|
||||
@@ -80,9 +76,6 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch("scroll", function (scroll) {
|
||||
bounds = scroll;
|
||||
});
|
||||
$scope.$watch("domainObject", initializeZoom);
|
||||
|
||||
return {
|
||||
@@ -100,9 +93,10 @@ define(
|
||||
zoom: function (amount) {
|
||||
// Update the zoom level if called with an argument
|
||||
if (arguments.length > 0 && !isNaN(amount)) {
|
||||
var bounds = $scope.scroll;
|
||||
var center = this.toMillis(bounds.x + bounds.width / 2);
|
||||
setZoomLevel(zoomIndex + amount);
|
||||
bounds.x = this.toPixels(center) - bounds.width / 2;
|
||||
setScroll(this.toPixels(center) - bounds.width / 2);
|
||||
}
|
||||
return zoomLevels[zoomIndex];
|
||||
},
|
||||
@@ -124,16 +118,14 @@ define(
|
||||
*/
|
||||
toMillis: toMillis,
|
||||
/**
|
||||
* Get or set the current displayed duration. If used as a
|
||||
* setter, this will typically be rounded up to ensure extra
|
||||
* space is available at the right.
|
||||
* @returns {number} duration, in milliseconds
|
||||
* Get the pixel width necessary to fit the specified
|
||||
* timestamp, expressed as an offset in milliseconds from
|
||||
* the start of the timeline.
|
||||
* @param {number} timestamp the time to display
|
||||
*/
|
||||
duration: function (value) {
|
||||
if (arguments.length > 0) {
|
||||
duration = roundDuration(value);
|
||||
}
|
||||
return duration;
|
||||
width: function (timestamp) {
|
||||
var pixels = Math.ceil(toPixels(timestamp * (1 + PADDING)));
|
||||
return Math.max($scope.scroll.width, pixels);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,13 +23,20 @@
|
||||
define(
|
||||
['../../src/actions/CompositionColumn'],
|
||||
function (CompositionColumn) {
|
||||
var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f'];
|
||||
|
||||
describe("CompositionColumn", function () {
|
||||
var testIndex,
|
||||
testIdMap,
|
||||
column;
|
||||
|
||||
beforeEach(function () {
|
||||
testIndex = 3;
|
||||
column = new CompositionColumn(testIndex);
|
||||
testIdMap = TEST_IDS.reduce(function (map, id, index) {
|
||||
map[id] = index;
|
||||
return map;
|
||||
}, {});
|
||||
column = new CompositionColumn(testIndex, testIdMap);
|
||||
});
|
||||
|
||||
it("includes a one-based index in its name", function () {
|
||||
@@ -46,15 +53,13 @@ define(
|
||||
'domainObject',
|
||||
['getId', 'getModel', 'getCapability']
|
||||
);
|
||||
testModel = {
|
||||
composition: ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
};
|
||||
testModel = { composition: TEST_IDS };
|
||||
mockDomainObject.getModel.andReturn(testModel);
|
||||
});
|
||||
|
||||
it("returns a corresponding identifier", function () {
|
||||
it("returns a corresponding value from the map", function () {
|
||||
expect(column.value(mockDomainObject))
|
||||
.toEqual(testModel.composition[testIndex]);
|
||||
.toEqual(testIdMap[testModel.composition[testIndex]]);
|
||||
});
|
||||
|
||||
it("returns nothing when composition is exceeded", function () {
|
||||
|
||||
@@ -24,7 +24,8 @@ define(
|
||||
['../../src/actions/ExportTimelineAsCSVAction'],
|
||||
function (ExportTimelineAsCSVAction) {
|
||||
describe("ExportTimelineAsCSVAction", function () {
|
||||
var mockExportService,
|
||||
var mockLog,
|
||||
mockExportService,
|
||||
mockNotificationService,
|
||||
mockNotification,
|
||||
mockDomainObject,
|
||||
@@ -39,6 +40,13 @@ define(
|
||||
['getId', 'getModel', 'getCapability', 'hasCapability']
|
||||
);
|
||||
mockType = jasmine.createSpyObj('type', ['instanceOf']);
|
||||
|
||||
mockLog = jasmine.createSpyObj('$log', [
|
||||
'warn',
|
||||
'error',
|
||||
'info',
|
||||
'debug'
|
||||
]);
|
||||
mockExportService = jasmine.createSpyObj(
|
||||
'exportService',
|
||||
['exportCSV']
|
||||
@@ -63,8 +71,10 @@ define(
|
||||
testContext = { domainObject: mockDomainObject };
|
||||
|
||||
action = new ExportTimelineAsCSVAction(
|
||||
mockLog,
|
||||
mockExportService,
|
||||
mockNotificationService,
|
||||
[],
|
||||
testContext
|
||||
);
|
||||
});
|
||||
@@ -129,8 +139,11 @@ define(
|
||||
});
|
||||
|
||||
describe("and an error occurs", function () {
|
||||
var testError;
|
||||
|
||||
beforeEach(function () {
|
||||
testPromise.reject();
|
||||
testError = { someProperty: "some value" };
|
||||
testPromise.reject(testError);
|
||||
waitsFor(function () {
|
||||
return mockCallback.calls.length > 0;
|
||||
});
|
||||
@@ -145,6 +158,10 @@ define(
|
||||
expect(mockNotificationService.error)
|
||||
.toHaveBeenCalledWith(jasmine.any(String));
|
||||
});
|
||||
|
||||
it("logs the root cause", function () {
|
||||
expect(mockLog.warn).toHaveBeenCalledWith(testError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@ define(
|
||||
|
||||
task = new ExportTimelineAsCSVTask(
|
||||
mockExportService,
|
||||
[],
|
||||
mockDomainObject
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,10 +24,12 @@ define(
|
||||
['../../src/actions/IdColumn'],
|
||||
function (IdColumn) {
|
||||
describe("IdColumn", function () {
|
||||
var column;
|
||||
var testIdMap,
|
||||
column;
|
||||
|
||||
beforeEach(function () {
|
||||
column = new IdColumn();
|
||||
testIdMap = { "foo": "bar" };
|
||||
column = new IdColumn(testIdMap);
|
||||
});
|
||||
|
||||
it("has a name", function () {
|
||||
@@ -47,9 +49,9 @@ define(
|
||||
mockDomainObject.getId.andReturn(testId);
|
||||
});
|
||||
|
||||
it("provides a domain object's identifier", function () {
|
||||
it("provides a value mapped from domain object's identifier", function () {
|
||||
expect(column.value(mockDomainObject))
|
||||
.toEqual(testId);
|
||||
.toEqual(testIdMap[testId]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,13 +23,20 @@
|
||||
define(
|
||||
['../../src/actions/ModeColumn'],
|
||||
function (ModeColumn) {
|
||||
var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f'];
|
||||
|
||||
describe("ModeColumn", function () {
|
||||
var testIndex,
|
||||
testIdMap,
|
||||
column;
|
||||
|
||||
beforeEach(function () {
|
||||
testIndex = 3;
|
||||
column = new ModeColumn(testIndex);
|
||||
testIdMap = TEST_IDS.reduce(function (map, id, index) {
|
||||
map[id] = index;
|
||||
return map;
|
||||
}, {});
|
||||
column = new ModeColumn(testIndex, testIdMap);
|
||||
});
|
||||
|
||||
it("includes a one-based index in its name", function () {
|
||||
@@ -48,15 +55,15 @@ define(
|
||||
);
|
||||
testModel = {
|
||||
relationships: {
|
||||
modes: ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
modes: TEST_IDS
|
||||
}
|
||||
};
|
||||
mockDomainObject.getModel.andReturn(testModel);
|
||||
});
|
||||
|
||||
it("returns a corresponding identifier", function () {
|
||||
it("returns a corresponding value from the map", function () {
|
||||
expect(column.value(mockDomainObject))
|
||||
.toEqual(testModel.relationships.modes[testIndex]);
|
||||
.toEqual(testIdMap[testModel.relationships.modes[testIndex]]);
|
||||
});
|
||||
|
||||
it("returns nothing when relationships are exceeded", function () {
|
||||
|
||||
@@ -75,7 +75,7 @@ define(
|
||||
return c === 'metadata' && testMetadata;
|
||||
});
|
||||
|
||||
exporter = new TimelineColumnizer(mockDomainObjects);
|
||||
exporter = new TimelineColumnizer(mockDomainObjects, []);
|
||||
});
|
||||
|
||||
describe("rows", function () {
|
||||
@@ -94,13 +94,6 @@ define(
|
||||
it("include one row per domain object", function () {
|
||||
expect(rows.length).toEqual(mockDomainObjects.length);
|
||||
});
|
||||
|
||||
it("includes identifiers for each domain object", function () {
|
||||
rows.forEach(function (row, index) {
|
||||
var id = mockDomainObjects[index].getId();
|
||||
expect(row.indexOf(id)).not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("headers", function () {
|
||||
|
||||
@@ -214,23 +214,6 @@ define(
|
||||
|
||||
});
|
||||
|
||||
it("reports full scrollable width using zoom controller", function () {
|
||||
var mockZoom = jasmine.createSpyObj('zoom', ['toPixels', 'duration']);
|
||||
mockZoom.toPixels.andReturn(54321);
|
||||
mockZoom.duration.andReturn(12345);
|
||||
|
||||
// Initially populate
|
||||
fireWatch('domainObject', mockDomainObject);
|
||||
|
||||
expect(controller.width(mockZoom)).toEqual(54321);
|
||||
// Verify interactions; we took zoom's duration for our start/end,
|
||||
// and converted it to pixels.
|
||||
// First, check that we used the start/end (from above)
|
||||
expect(mockZoom.duration).toHaveBeenCalledWith(12321 - 42);
|
||||
// Next, verify that the result was passed to toPixels
|
||||
expect(mockZoom.toPixels).toHaveBeenCalledWith(12345);
|
||||
});
|
||||
|
||||
it("provides drag handles", function () {
|
||||
// TimelineDragPopulator et al are tested for these,
|
||||
// so just verify that handles are indeed exposed.
|
||||
|
||||
@@ -28,6 +28,7 @@ define(
|
||||
describe("The timeline zoom state controller", function () {
|
||||
var testConfiguration,
|
||||
mockScope,
|
||||
mockTimeout,
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -37,8 +38,11 @@ define(
|
||||
};
|
||||
mockScope = jasmine.createSpyObj("$scope", ['$watch']);
|
||||
mockScope.commit = jasmine.createSpy('commit');
|
||||
mockScope.scroll = { x: 0, width: 1000 };
|
||||
mockTimeout = jasmine.createSpy('$timeout');
|
||||
controller = new TimelineZoomController(
|
||||
mockScope,
|
||||
mockTimeout,
|
||||
testConfiguration
|
||||
);
|
||||
});
|
||||
@@ -47,12 +51,6 @@ define(
|
||||
expect(controller.zoom()).toEqual(2000);
|
||||
});
|
||||
|
||||
it("allows duration to be changed", function () {
|
||||
var initial = controller.duration();
|
||||
controller.duration(initial * 3.33);
|
||||
expect(controller.duration() > initial).toBeTruthy();
|
||||
});
|
||||
|
||||
it("handles time-to-pixel conversions", function () {
|
||||
var zoomLevel = controller.zoom();
|
||||
expect(controller.toPixels(zoomLevel)).toEqual(12321);
|
||||
@@ -70,11 +68,6 @@ define(
|
||||
expect(controller.zoom()).toEqual(3500);
|
||||
});
|
||||
|
||||
it("observes scroll bounds", function () {
|
||||
expect(mockScope.$watch)
|
||||
.toHaveBeenCalledWith("scroll", jasmine.any(Function));
|
||||
});
|
||||
|
||||
describe("when watches have fired", function () {
|
||||
var mockDomainObject,
|
||||
mockPromise,
|
||||
@@ -115,6 +108,10 @@ define(
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
call.args[1](mockScope[call.args[0]]);
|
||||
});
|
||||
|
||||
mockTimeout.calls.forEach(function (call) {
|
||||
call.args[0]();
|
||||
});
|
||||
});
|
||||
|
||||
it("zooms to fit the timeline", function () {
|
||||
@@ -125,6 +122,27 @@ define(
|
||||
expect(Math.round(controller.toMillis(x2)))
|
||||
.toBeGreaterThan(testEnd);
|
||||
});
|
||||
|
||||
it("provides a width which is not less than scroll area width", function () {
|
||||
var testPixel = mockScope.scroll.width / 4,
|
||||
testMillis = controller.toMillis(testPixel);
|
||||
expect(controller.width(testMillis))
|
||||
.not.toBeLessThan(mockScope.scroll.width);
|
||||
});
|
||||
|
||||
it("provides a width with some margin past timestamp", function () {
|
||||
var testPixel = mockScope.scroll.width * 4,
|
||||
testMillis = controller.toMillis(testPixel);
|
||||
expect(controller.width(testMillis))
|
||||
.toBeGreaterThan(controller.toPixels(testMillis));
|
||||
});
|
||||
|
||||
it("provides a width which does not greatly exceed timestamp", function () {
|
||||
var testPixel = mockScope.scroll.width * 4,
|
||||
testMillis = controller.toMillis(testPixel);
|
||||
expect(controller.width(testMillis))
|
||||
.toBeLessThan(controller.toPixels(testMillis * 2));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
70
src/MCT.js
70
src/MCT.js
@@ -1,70 +0,0 @@
|
||||
define([
|
||||
'EventEmitter',
|
||||
'legacyRegistry',
|
||||
'./api/api'
|
||||
], function (EventEmitter, legacyRegistry, api) {
|
||||
function MCT() {
|
||||
EventEmitter.call(this);
|
||||
this.legacyBundle = { extensions: {} };
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
Object.keys(api).forEach(function (k) {
|
||||
MCT.prototype[k] = api[k];
|
||||
});
|
||||
MCT.prototype.MCT = MCT;
|
||||
|
||||
MCT.prototype.type = function (key, type) {
|
||||
var legacyDef = type.toLegacyDefinition();
|
||||
legacyDef.key = key;
|
||||
this.legacyBundle.extensions.types =
|
||||
this.legacyBundle.extensions.types || [];
|
||||
this.legacyBundle.extensions.types.push(legacyDef);
|
||||
|
||||
var viewFactory = type.view(this.regions.main);
|
||||
if (viewFactory) {
|
||||
var viewKey = key + "." + this.regions.main;
|
||||
|
||||
this.legacyBundle.extensions.views =
|
||||
this.legacyBundle.extensions.views || [];
|
||||
this.legacyBundle.extensions.views.push({
|
||||
name: "A view",
|
||||
key: "adapted-view",
|
||||
template: '<mct-view key="\'' +
|
||||
viewKey +
|
||||
'\'" ' +
|
||||
'mct-object="domainObject">' +
|
||||
'</mct-view>'
|
||||
});
|
||||
|
||||
this.legacyBundle.extensions.newViews =
|
||||
this.legacyBundle.extensions.newViews || [];
|
||||
this.legacyBundle.extensions.newViews.push({
|
||||
factory: viewFactory,
|
||||
key: viewKey
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MCT.prototype.start = function () {
|
||||
legacyRegistry.register('adapter', this.legacyBundle);
|
||||
this.emit('start');
|
||||
};
|
||||
|
||||
MCT.prototype.regions = {
|
||||
main: "MAIN"
|
||||
};
|
||||
|
||||
MCT.prototype.verbs = {
|
||||
mutate: function (domainObject, mutator) {
|
||||
return domainObject.useCapability('mutation', mutator)
|
||||
.then(function () {
|
||||
var persistence = domainObject.getCapability('persistence');
|
||||
return persistence.persist();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return MCT;
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
define([
|
||||
'legacyRegistry',
|
||||
'./directives/MCTView'
|
||||
], function (legacyRegistry, MCTView) {
|
||||
legacyRegistry.register('src/adapter', {
|
||||
"extensions": {
|
||||
"directives": [
|
||||
{
|
||||
key: "mctView",
|
||||
implementation: MCTView,
|
||||
depends: ["newViews[]"]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
define(['angular'], function (angular) {
|
||||
function MCTView(newViews) {
|
||||
var factories = {};
|
||||
|
||||
newViews.forEach(function (newView) {
|
||||
factories[newView.key] = newView.factory;
|
||||
});
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function (scope, element, attrs) {
|
||||
var key = undefined;
|
||||
var mctObject = undefined;
|
||||
|
||||
function maybeShow() {
|
||||
if (!factories[key]) {
|
||||
return;
|
||||
}
|
||||
if (!mctObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
var view = factories[key](mctObject);
|
||||
var elements = view.elements();
|
||||
element.empty();
|
||||
element.append(elements);
|
||||
}
|
||||
|
||||
function setKey(k) {
|
||||
key = k;
|
||||
maybeShow();
|
||||
}
|
||||
|
||||
function setObject(obj) {
|
||||
mctObject = obj;
|
||||
maybeShow();
|
||||
}
|
||||
|
||||
scope.$watch('key', setKey);
|
||||
scope.$watch('mctObject', setObject);
|
||||
|
||||
},
|
||||
scope: {
|
||||
key: "=",
|
||||
mctObject: "="
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return MCTView;
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
define(function () {
|
||||
/**
|
||||
* @typedef TypeDefinition
|
||||
* @property {Metadata} metadata displayable metadata about this type
|
||||
* @property {function (object)} [initialize] a function which initializes
|
||||
* the model for new domain objects of this type
|
||||
* @property {boolean} [creatable] true if users should be allowed to
|
||||
* create this type (default: false)
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TypeDefinition} definition
|
||||
* @constructor
|
||||
*/
|
||||
function Type(definition) {
|
||||
this.definition = definition;
|
||||
this.views = {};
|
||||
}
|
||||
|
||||
Type.prototype.view = function (region, factory) {
|
||||
if (arguments.length > 1) {
|
||||
this.views[region] = factory;
|
||||
}
|
||||
return this.views[region];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a definition for this type that can be registered using the
|
||||
* legacy bundle format.
|
||||
* @private
|
||||
*/
|
||||
Type.prototype.toLegacyDefinition = function () {
|
||||
var def = {};
|
||||
def.name = this.definition.metadata.label;
|
||||
def.glyph = this.definition.metadata.glyph;
|
||||
def.description = this.definition.metadata.description;
|
||||
|
||||
if (this.definition.initialize) {
|
||||
def.model = {};
|
||||
this.definition.initialize(def.model);
|
||||
}
|
||||
|
||||
if (this.definition.creatable) {
|
||||
def.features = ['creation'];
|
||||
}
|
||||
return def;
|
||||
};
|
||||
|
||||
return Type;
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
define(['EventEmitter'], function (EventEmitter) {
|
||||
function View() {
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
|
||||
View.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
['elements', 'model'].forEach(function (method) {
|
||||
View.prototype[method] = function (value) {
|
||||
this.viewState =
|
||||
this.viewState || { elements: [], model: undefined };
|
||||
if (arguments.length > 0) {
|
||||
this.viewState[method] = value;
|
||||
this.emit(method, value);
|
||||
}
|
||||
return this.viewState[method];
|
||||
}
|
||||
});
|
||||
|
||||
return View;
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
define([
|
||||
'./Type',
|
||||
'./View'
|
||||
], function (
|
||||
Type,
|
||||
View
|
||||
) {
|
||||
return {
|
||||
Type: Type,
|
||||
View: View
|
||||
};
|
||||
});
|
||||
@@ -48,7 +48,6 @@ requirejs.config({
|
||||
"angular-route": "bower_components/angular-route/angular-route.min",
|
||||
"csv": "bower_components/comma-separated-values/csv.min",
|
||||
"es6-promise": "bower_components/es6-promise/promise.min",
|
||||
"EventEmitter": "bower_components/eventemitter3/index",
|
||||
"moment": "bower_components/moment/moment",
|
||||
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
|
||||
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
|
||||
@@ -65,9 +64,6 @@ requirejs.config({
|
||||
"angular-route": {
|
||||
"deps": [ "angular" ]
|
||||
},
|
||||
"EventEmitter": {
|
||||
"exports": "EventEmitter"
|
||||
},
|
||||
"moment-duration-format": {
|
||||
"deps": [ "moment" ]
|
||||
},
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
/*global require,process,console*/
|
||||
|
||||
var CONFIG = {
|
||||
port: 8081,
|
||||
dictionary: "dictionary.json",
|
||||
interval: 1000
|
||||
};
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var WebSocketServer = require('ws').Server,
|
||||
fs = require('fs'),
|
||||
wss = new WebSocketServer({ port: CONFIG.port }),
|
||||
dictionary = JSON.parse(fs.readFileSync(CONFIG.dictionary, "utf8")),
|
||||
spacecraft = {
|
||||
"prop.fuel": 77,
|
||||
"prop.thrusters": "OFF",
|
||||
"comms.recd": 0,
|
||||
"comms.sent": 0,
|
||||
"pwr.temp": 245,
|
||||
"pwr.c": 8.15,
|
||||
"pwr.v": 30
|
||||
},
|
||||
histories = {},
|
||||
listeners = [];
|
||||
|
||||
function updateSpacecraft() {
|
||||
spacecraft["prop.fuel"] = Math.max(
|
||||
0,
|
||||
spacecraft["prop.fuel"] -
|
||||
(spacecraft["prop.thrusters"] === "ON" ? 0.5 : 0)
|
||||
);
|
||||
spacecraft["pwr.temp"] = spacecraft["pwr.temp"] * 0.985
|
||||
+ Math.random() * 0.25 + Math.sin(Date.now());
|
||||
spacecraft["pwr.c"] = spacecraft["pwr.c"] * 0.985;
|
||||
spacecraft["pwr.v"] = 30 + Math.pow(Math.random(), 3);
|
||||
}
|
||||
|
||||
function generateTelemetry() {
|
||||
var timestamp = Date.now(), sent = 0;
|
||||
Object.keys(spacecraft).forEach(function (id) {
|
||||
var state = { timestamp: timestamp, value: spacecraft[id] };
|
||||
histories[id] = histories[id] || []; // Initialize
|
||||
histories[id].push(state);
|
||||
spacecraft["comms.sent"] += JSON.stringify(state).length;
|
||||
});
|
||||
listeners.forEach(function (listener) {
|
||||
listener();
|
||||
});
|
||||
}
|
||||
|
||||
function update() {
|
||||
updateSpacecraft();
|
||||
generateTelemetry();
|
||||
}
|
||||
|
||||
function handleConnection(ws) {
|
||||
var subscriptions = {}, // Active subscriptions for this connection
|
||||
handlers = { // Handlers for specific requests
|
||||
dictionary: function () {
|
||||
ws.send(JSON.stringify({
|
||||
type: "dictionary",
|
||||
value: dictionary
|
||||
}));
|
||||
},
|
||||
subscribe: function (id) {
|
||||
subscriptions[id] = true;
|
||||
},
|
||||
unsubscribe: function (id) {
|
||||
delete subscriptions[id];
|
||||
},
|
||||
history: function (id) {
|
||||
ws.send(JSON.stringify({
|
||||
type: "history",
|
||||
id: id,
|
||||
value: histories[id]
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
function notifySubscribers() {
|
||||
Object.keys(subscriptions).forEach(function (id) {
|
||||
var history = histories[id];
|
||||
if (history) {
|
||||
ws.send(JSON.stringify({
|
||||
type: "data",
|
||||
id: id,
|
||||
value: history[history.length - 1]
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for requests
|
||||
ws.on('message', function (message) {
|
||||
var parts = message.split(' '),
|
||||
handler = handlers[parts[0]];
|
||||
if (handler) {
|
||||
handler.apply(handlers, parts.slice(1));
|
||||
}
|
||||
});
|
||||
|
||||
// Stop sending telemetry updates for this connection when closed
|
||||
ws.on('close', function () {
|
||||
listeners = listeners.filter(function (listener) {
|
||||
return listener !== notifySubscribers;
|
||||
});
|
||||
});
|
||||
|
||||
// Notify subscribers when telemetry is updated
|
||||
listeners.push(notifySubscribers);
|
||||
}
|
||||
|
||||
update();
|
||||
setInterval(update, CONFIG.interval);
|
||||
|
||||
wss.on('connection', handleConnection);
|
||||
|
||||
console.log("Example spacecraft running on port ");
|
||||
console.log("Press Enter to toggle thruster state.");
|
||||
process.stdin.on('data', function (data) {
|
||||
spacecraft['prop.thrusters'] =
|
||||
(spacecraft['prop.thrusters'] === "OFF") ? "ON" : "OFF";
|
||||
console.log("Thrusters " + spacecraft["prop.thrusters"]);
|
||||
});
|
||||
}());
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"name": "Example Spacecraft",
|
||||
"identifier": "sc",
|
||||
"subsystems": [
|
||||
{
|
||||
"name": "Propulsion",
|
||||
"identifier": "prop",
|
||||
"measurements": [
|
||||
{
|
||||
"name": "Fuel",
|
||||
"identifier": "prop.fuel",
|
||||
"units": "kilograms",
|
||||
"type": "float"
|
||||
},
|
||||
{
|
||||
"name": "Thrusters",
|
||||
"identifier": "prop.thrusters",
|
||||
"units": "None",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Communications",
|
||||
"identifier": "comms",
|
||||
"measurements": [
|
||||
{
|
||||
"name": "Received",
|
||||
"identifier": "comms.recd",
|
||||
"units": "bytes",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "Sent",
|
||||
"identifier": "comms.sent",
|
||||
"units": "bytes",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Power",
|
||||
"identifier": "pwr",
|
||||
"measurements": [
|
||||
{
|
||||
"name": "Generator Temperature",
|
||||
"identifier": "pwr.temp",
|
||||
"units": "\u0080C",
|
||||
"type": "float"
|
||||
},
|
||||
{
|
||||
"name": "Generator Current",
|
||||
"identifier": "pwr.c",
|
||||
"units": "A",
|
||||
"type": "float"
|
||||
},
|
||||
{
|
||||
"name": "Generator Voltage",
|
||||
"identifier": "pwr.v",
|
||||
"units": "V",
|
||||
"type": "float"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
define([
|
||||
'legacyRegistry',
|
||||
'./src/controllers/BarGraphController'
|
||||
], function (
|
||||
legacyRegistry,
|
||||
BarGraphController
|
||||
) {
|
||||
legacyRegistry.register("tutorials/bargraph", {
|
||||
"name": "Bar Graph",
|
||||
"description": "Provides the Bar Graph view of telemetry elements.",
|
||||
"extensions": {
|
||||
"views": [
|
||||
{
|
||||
"name": "Bar Graph",
|
||||
"key": "example.bargraph",
|
||||
"glyph": "H",
|
||||
"templateUrl": "templates/bargraph.html",
|
||||
"needs": [ "telemetry" ],
|
||||
"delegation": true,
|
||||
"editable": true,
|
||||
"toolbar": {
|
||||
"sections": [
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "Low",
|
||||
"property": "low",
|
||||
"required": true,
|
||||
"control": "textfield",
|
||||
"size": 4
|
||||
},
|
||||
{
|
||||
"name": "Middle",
|
||||
"property": "middle",
|
||||
"required": true,
|
||||
"control": "textfield",
|
||||
"size": 4
|
||||
},
|
||||
{
|
||||
"name": "High",
|
||||
"property": "high",
|
||||
"required": true,
|
||||
"control": "textfield",
|
||||
"size": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"stylesheets": [
|
||||
{
|
||||
"stylesheetUrl": "css/bargraph.css"
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "BarGraphController",
|
||||
"implementation": BarGraphController,
|
||||
"depends": [ "$scope", "telemetryHandler" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
<div class="example-bargraph" ng-controller="BarGraphController">
|
||||
<div class="example-tick-labels">
|
||||
<div ng-repeat="value in [low, middle, high] track by $index"
|
||||
class="example-tick-label"
|
||||
style="bottom: {{ toPercent(value) }}%">
|
||||
{{value}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-graph-area">
|
||||
<div ng-repeat="telemetryObject in telemetryObjects"
|
||||
style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
|
||||
class="example-bar-holder">
|
||||
<div class="example-bar"
|
||||
ng-style="{
|
||||
bottom: getBottom(telemetryObject) + '%',
|
||||
top: getTop(telemetryObject) + '%'
|
||||
}">
|
||||
</div>
|
||||
</div>
|
||||
<div style="bottom: {{ toPercent(middle) }}%"
|
||||
class="example-graph-tick">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-bar-labels">
|
||||
<div ng-repeat="telemetryObject in telemetryObjects"
|
||||
style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
|
||||
class="example-bar-holder example-label">
|
||||
<mct-representation key="'label'"
|
||||
mct-object="telemetryObject">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,75 +0,0 @@
|
||||
define(function () {
|
||||
function BarGraphController($scope, telemetryHandler) {
|
||||
var handle;
|
||||
|
||||
// Expose configuration constants directly in scope
|
||||
function exposeConfiguration() {
|
||||
$scope.low = $scope.configuration.low;
|
||||
$scope.middle = $scope.configuration.middle;
|
||||
$scope.high = $scope.configuration.high;
|
||||
}
|
||||
|
||||
// Populate a default value in the configuration
|
||||
function setDefault(key, value) {
|
||||
if ($scope.configuration[key] === undefined) {
|
||||
$scope.configuration[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Getter-setter for configuration properties (for view proxy)
|
||||
function getterSetter(property) {
|
||||
return function (value) {
|
||||
value = parseFloat(value);
|
||||
if (!isNaN(value)) {
|
||||
$scope.configuration[property] = value;
|
||||
exposeConfiguration();
|
||||
}
|
||||
return $scope.configuration[property];
|
||||
};
|
||||
}
|
||||
|
||||
// Add min/max defaults
|
||||
setDefault('low', -1);
|
||||
setDefault('middle', 0);
|
||||
setDefault('high', 1);
|
||||
exposeConfiguration($scope.configuration);
|
||||
|
||||
// Expose view configuration options
|
||||
if ($scope.selection) {
|
||||
$scope.selection.proxy({
|
||||
low: getterSetter('low'),
|
||||
middle: getterSetter('middle'),
|
||||
high: getterSetter('high')
|
||||
});
|
||||
}
|
||||
|
||||
// Convert value to a percent between 0-100
|
||||
$scope.toPercent = function (value) {
|
||||
var pct = 100 * (value - $scope.low) /
|
||||
($scope.high - $scope.low);
|
||||
return Math.min(100, Math.max(0, pct));
|
||||
};
|
||||
|
||||
// Get bottom and top (as percentages) for current value
|
||||
$scope.getBottom = function (telemetryObject) {
|
||||
var value = handle.getRangeValue(telemetryObject);
|
||||
return $scope.toPercent(Math.min($scope.middle, value));
|
||||
};
|
||||
$scope.getTop = function (telemetryObject) {
|
||||
var value = handle.getRangeValue(telemetryObject);
|
||||
return 100 - $scope.toPercent(Math.max($scope.middle, value));
|
||||
};
|
||||
|
||||
// Use the telemetryHandler to get telemetry objects here
|
||||
handle = telemetryHandler.handle($scope.domainObject, function () {
|
||||
$scope.telemetryObjects = handle.getTelemetryObjects();
|
||||
$scope.barWidth =
|
||||
100 / Math.max(($scope.telemetryObjects).length, 1);
|
||||
});
|
||||
|
||||
// Release subscriptions when scope is destroyed
|
||||
$scope.$on('$destroy', handle.unsubscribe);
|
||||
}
|
||||
|
||||
return BarGraphController;
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
define([
|
||||
'legacyRegistry',
|
||||
'./src/ExampleTelemetryServerAdapter',
|
||||
'./src/ExampleTelemetryInitializer',
|
||||
'./src/ExampleTelemetryModelProvider'
|
||||
], function (
|
||||
legacyRegistry,
|
||||
ExampleTelemetryServerAdapter,
|
||||
ExampleTelemetryInitializer,
|
||||
ExampleTelemetryModelProvider
|
||||
) {
|
||||
legacyRegistry.register("tutorials/telemetry", {
|
||||
"name": "Example Telemetry Adapter",
|
||||
"extensions": {
|
||||
"types": [
|
||||
{
|
||||
"name": "Spacecraft",
|
||||
"key": "example.spacecraft",
|
||||
"glyph": "o"
|
||||
},
|
||||
{
|
||||
"name": "Subsystem",
|
||||
"key": "example.subsystem",
|
||||
"glyph": "o",
|
||||
"model": { "composition": [] }
|
||||
},
|
||||
{
|
||||
"name": "Measurement",
|
||||
"key": "example.measurement",
|
||||
"glyph": "T",
|
||||
"model": { "telemetry": {} },
|
||||
"telemetry": {
|
||||
"source": "example.source",
|
||||
"domains": [
|
||||
{
|
||||
"name": "Time",
|
||||
"key": "timestamp"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"roots": [
|
||||
{
|
||||
"id": "example:sc",
|
||||
"priority": "preferred",
|
||||
"model": {
|
||||
"type": "example.spacecraft",
|
||||
"name": "My Spacecraft",
|
||||
"composition": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"key": "example.adapter",
|
||||
"implementation": "ExampleTelemetryServerAdapter.js",
|
||||
"depends": [ "$q", "EXAMPLE_WS_URL" ]
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "EXAMPLE_WS_URL",
|
||||
"priority": "fallback",
|
||||
"value": "ws://localhost:8081"
|
||||
}
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"implementation": "ExampleTelemetryInitializer.js",
|
||||
"depends": [ "example.adapter", "objectService" ]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"provides": "modelService",
|
||||
"type": "provider",
|
||||
"implementation": "ExampleTelemetryModelProvider.js",
|
||||
"depends": [ "example.adapter", "$q" ]
|
||||
},
|
||||
{
|
||||
"provides": "telemetryService",
|
||||
"type": "provider",
|
||||
"implementation": "ExampleTelemetryProvider.js",
|
||||
"depends": [ "example.adapter", "$q" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
define(
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
var TAXONOMY_ID = "example:sc",
|
||||
PREFIX = "example_tlm:";
|
||||
|
||||
function ExampleTelemetryInitializer(adapter, objectService) {
|
||||
// Generate a domain object identifier for a dictionary element
|
||||
function makeId(element) {
|
||||
return PREFIX + element.identifier;
|
||||
}
|
||||
|
||||
// When the dictionary is available, add all subsystems
|
||||
// to the composition of My Spacecraft
|
||||
function initializeTaxonomy(dictionary) {
|
||||
// Get the top-level container for dictionary objects
|
||||
// from a group of domain objects.
|
||||
function getTaxonomyObject(domainObjects) {
|
||||
return domainObjects[TAXONOMY_ID];
|
||||
}
|
||||
|
||||
// Populate
|
||||
function populateModel(taxonomyObject) {
|
||||
return taxonomyObject.useCapability(
|
||||
"mutation",
|
||||
function (model) {
|
||||
model.name =
|
||||
dictionary.name;
|
||||
model.composition =
|
||||
dictionary.subsystems.map(makeId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Look up My Spacecraft, and populate it accordingly.
|
||||
objectService.getObjects([TAXONOMY_ID])
|
||||
.then(getTaxonomyObject)
|
||||
.then(populateModel);
|
||||
}
|
||||
|
||||
adapter.dictionary().then(initializeTaxonomy);
|
||||
}
|
||||
|
||||
return ExampleTelemetryInitializer;
|
||||
}
|
||||
);
|
||||
@@ -1,78 +0,0 @@
|
||||
define(
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
var PREFIX = "example_tlm:",
|
||||
FORMAT_MAPPINGS = {
|
||||
float: "number",
|
||||
integer: "number",
|
||||
string: "string"
|
||||
};
|
||||
|
||||
function ExampleTelemetryModelProvider(adapter, $q) {
|
||||
var modelPromise, empty = $q.when({});
|
||||
|
||||
// Check if this model is in our dictionary (by prefix)
|
||||
function isRelevant(id) {
|
||||
return id.indexOf(PREFIX) === 0;
|
||||
}
|
||||
|
||||
// Build a domain object identifier by adding a prefix
|
||||
function makeId(element) {
|
||||
return PREFIX + element.identifier;
|
||||
}
|
||||
|
||||
// Create domain object models from this dictionary
|
||||
function buildTaxonomy(dictionary) {
|
||||
var models = {};
|
||||
|
||||
// Create & store a domain object model for a measurement
|
||||
function addMeasurement(measurement) {
|
||||
var format = FORMAT_MAPPINGS[measurement.type];
|
||||
models[makeId(measurement)] = {
|
||||
type: "example.measurement",
|
||||
name: measurement.name,
|
||||
telemetry: {
|
||||
key: measurement.identifier,
|
||||
ranges: [{
|
||||
key: "value",
|
||||
name: "Value",
|
||||
units: measurement.units,
|
||||
format: format
|
||||
}]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Create & store a domain object model for a subsystem
|
||||
function addSubsystem(subsystem) {
|
||||
var measurements =
|
||||
(subsystem.measurements || []);
|
||||
models[makeId(subsystem)] = {
|
||||
type: "example.subsystem",
|
||||
name: subsystem.name,
|
||||
composition: measurements.map(makeId)
|
||||
};
|
||||
measurements.forEach(addMeasurement);
|
||||
}
|
||||
|
||||
(dictionary.subsystems || []).forEach(addSubsystem);
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
// Begin generating models once the dictionary is available
|
||||
modelPromise = adapter.dictionary().then(buildTaxonomy);
|
||||
|
||||
return {
|
||||
getModels: function (ids) {
|
||||
// Return models for the dictionary only when they
|
||||
// are relevant to the request.
|
||||
return ids.some(isRelevant) ? modelPromise : empty;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return ExampleTelemetryModelProvider;
|
||||
}
|
||||
);
|
||||
@@ -1,80 +0,0 @@
|
||||
define(
|
||||
['./ExampleTelemetrySeries'],
|
||||
function (ExampleTelemetrySeries) {
|
||||
"use strict";
|
||||
|
||||
var SOURCE = "example.source";
|
||||
|
||||
function ExampleTelemetryProvider(adapter, $q) {
|
||||
var subscribers = {};
|
||||
|
||||
// Used to filter out requests for telemetry
|
||||
// from some other source
|
||||
function matchesSource(request) {
|
||||
return (request.source === SOURCE);
|
||||
}
|
||||
|
||||
// Listen for data, notify subscribers
|
||||
adapter.listen(function (message) {
|
||||
var packaged = {};
|
||||
packaged[SOURCE] = {};
|
||||
packaged[SOURCE][message.id] =
|
||||
new ExampleTelemetrySeries([message.value]);
|
||||
(subscribers[message.id] || []).forEach(function (cb) {
|
||||
cb(packaged);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
requestTelemetry: function (requests) {
|
||||
var packaged = {},
|
||||
relevantReqs = requests.filter(matchesSource);
|
||||
|
||||
// Package historical telemetry that has been received
|
||||
function addToPackage(history) {
|
||||
packaged[SOURCE][history.id] =
|
||||
new ExampleTelemetrySeries(history.value);
|
||||
}
|
||||
|
||||
// Retrieve telemetry for a specific measurement
|
||||
function handleRequest(request) {
|
||||
var key = request.key;
|
||||
return adapter.history(key).then(addToPackage);
|
||||
}
|
||||
|
||||
packaged[SOURCE] = {};
|
||||
return $q.all(relevantReqs.map(handleRequest))
|
||||
.then(function () { return packaged; });
|
||||
},
|
||||
subscribe: function (callback, requests) {
|
||||
var keys = requests.filter(matchesSource)
|
||||
.map(function (req) { return req.key; });
|
||||
|
||||
function notCallback(cb) {
|
||||
return cb !== callback;
|
||||
}
|
||||
|
||||
function unsubscribe(key) {
|
||||
subscribers[key] =
|
||||
(subscribers[key] || []).filter(notCallback);
|
||||
if (subscribers[key].length < 1) {
|
||||
adapter.unsubscribe(key);
|
||||
}
|
||||
}
|
||||
|
||||
keys.forEach(function (key) {
|
||||
subscribers[key] = subscribers[key] || [];
|
||||
adapter.subscribe(key);
|
||||
subscribers[key].push(callback);
|
||||
});
|
||||
|
||||
return function () {
|
||||
keys.forEach(unsubscribe);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return ExampleTelemetryProvider;
|
||||
}
|
||||
);
|
||||
@@ -1,23 +0,0 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
function ExampleTelemetrySeries(data) {
|
||||
return {
|
||||
getPointCount: function () {
|
||||
return data.length;
|
||||
},
|
||||
getDomainValue: function (index) {
|
||||
return (data[index] || {}).timestamp;
|
||||
},
|
||||
getRangeValue: function (index) {
|
||||
return (data[index] || {}).value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return ExampleTelemetrySeries;
|
||||
}
|
||||
);
|
||||
@@ -1,60 +0,0 @@
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
function ExampleTelemetryServerAdapter($q, wsUrl) {
|
||||
var ws = new WebSocket(wsUrl),
|
||||
histories = {},
|
||||
listeners = [],
|
||||
dictionary = $q.defer();
|
||||
|
||||
// Handle an incoming message from the server
|
||||
ws.onmessage = function (event) {
|
||||
var message = JSON.parse(event.data);
|
||||
|
||||
switch (message.type) {
|
||||
case "dictionary":
|
||||
dictionary.resolve(message.value);
|
||||
break;
|
||||
case "history":
|
||||
histories[message.id].resolve(message);
|
||||
delete histories[message.id];
|
||||
break;
|
||||
case "data":
|
||||
listeners.forEach(function (listener) {
|
||||
listener(message);
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Request dictionary once connection is established
|
||||
ws.onopen = function () {
|
||||
ws.send("dictionary");
|
||||
};
|
||||
|
||||
return {
|
||||
dictionary: function () {
|
||||
return dictionary.promise;
|
||||
},
|
||||
history: function (id) {
|
||||
histories[id] = histories[id] || $q.defer();
|
||||
ws.send("history " + id);
|
||||
return histories[id].promise;
|
||||
},
|
||||
subscribe: function (id) {
|
||||
ws.send("subscribe " + id);
|
||||
},
|
||||
unsubscribe: function (id) {
|
||||
ws.send("unsubscribe " + id);
|
||||
},
|
||||
listen: function (callback) {
|
||||
listeners.push(callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return ExampleTelemetryServerAdapter;
|
||||
}
|
||||
);
|
||||
@@ -1,19 +0,0 @@
|
||||
define([
|
||||
'legacyRegistry',
|
||||
'./src/controllers/TodoController'
|
||||
], function (
|
||||
legacyRegistry,
|
||||
TodoController
|
||||
) {
|
||||
legacyRegistry.register("tutorials/todo", {
|
||||
"name": "To-do Plugin",
|
||||
"description": "Allows creating and editing to-do lists.",
|
||||
"extensions": {
|
||||
"stylesheets": [
|
||||
{
|
||||
"stylesheetUrl": "css/todo.css"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
<div ng-controller="TodoController" class="example-todo">
|
||||
<div class="example-button-group">
|
||||
<a ng-class="{ selected: checkVisibility(true) }"
|
||||
ng-click="setVisibility(true)">All</a>
|
||||
<a ng-class="{ selected: checkVisibility(false, false) }"
|
||||
ng-click="setVisibility(false, false)">Incomplete</a>
|
||||
<a ng-class="{ selected: checkVisibility(false, true) }"
|
||||
ng-click="setVisibility(false, true)">Complete</a>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li ng-repeat="task in model.tasks"
|
||||
ng-class="{ 'example-task-completed': task.completed }"
|
||||
ng-if="showTask(task)">
|
||||
|
||||
<input type="checkbox"
|
||||
ng-checked="task.completed"
|
||||
ng-click="toggleCompletion($index)">
|
||||
<span ng-click="selectTask($index)"
|
||||
ng-class="{ selected: isSelected($index) }"
|
||||
class="example-task-description">
|
||||
{{task.description}}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div ng-if="model.tasks.length < 1" class="example-message">
|
||||
There are no tasks to show.
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,97 +0,0 @@
|
||||
define(function () {
|
||||
// Form to display when adding new tasks
|
||||
var NEW_TASK_FORM = {
|
||||
name: "Add a Task",
|
||||
sections: [{
|
||||
rows: [{
|
||||
name: 'Description',
|
||||
key: 'description',
|
||||
control: 'textfield',
|
||||
required: true
|
||||
}]
|
||||
}]
|
||||
};
|
||||
|
||||
function TodoController($scope, dialogService) {
|
||||
var showAll = true,
|
||||
showCompleted;
|
||||
|
||||
// Persist changes made to a domain object's model
|
||||
function persist() {
|
||||
var persistence =
|
||||
$scope.domainObject.getCapability('persistence');
|
||||
return persistence && persistence.persist();
|
||||
}
|
||||
|
||||
// Remove a task
|
||||
function removeTaskAtIndex(taskIndex) {
|
||||
$scope.domainObject.useCapability('mutation', function (model) {
|
||||
model.tasks.splice(taskIndex, 1);
|
||||
});
|
||||
persist();
|
||||
}
|
||||
|
||||
// Add a task
|
||||
function addNewTask(task) {
|
||||
$scope.domainObject.useCapability('mutation', function (model) {
|
||||
model.tasks.push(task);
|
||||
});
|
||||
persist();
|
||||
}
|
||||
|
||||
// Change which tasks are visible
|
||||
$scope.setVisibility = function (all, completed) {
|
||||
showAll = all;
|
||||
showCompleted = completed;
|
||||
};
|
||||
|
||||
// Check if current visibility settings match
|
||||
$scope.checkVisibility = function (all, completed) {
|
||||
return showAll ? all : (completed === showCompleted);
|
||||
};
|
||||
|
||||
// Toggle the completion state of a task
|
||||
$scope.toggleCompletion = function (taskIndex) {
|
||||
$scope.domainObject.useCapability('mutation', function (model) {
|
||||
var task = model.tasks[taskIndex];
|
||||
task.completed = !task.completed;
|
||||
});
|
||||
persist();
|
||||
};
|
||||
|
||||
// Check whether a task should be visible
|
||||
$scope.showTask = function (task) {
|
||||
return showAll || (showCompleted === !!(task.completed));
|
||||
};
|
||||
|
||||
// Handle selection state in edit mode
|
||||
if ($scope.selection) {
|
||||
// Expose the ability to select tasks
|
||||
$scope.selectTask = function (taskIndex) {
|
||||
$scope.selection.select({
|
||||
removeTask: function () {
|
||||
removeTaskAtIndex(taskIndex);
|
||||
$scope.selection.deselect();
|
||||
},
|
||||
taskIndex: taskIndex
|
||||
});
|
||||
};
|
||||
|
||||
// Expose a check for current selection state
|
||||
$scope.isSelected = function (taskIndex) {
|
||||
return ($scope.selection.get() || {}).taskIndex ===
|
||||
taskIndex;
|
||||
};
|
||||
|
||||
// Expose a view-level selection proxy
|
||||
$scope.selection.proxy({
|
||||
addTask: function () {
|
||||
dialogService.getUserInput(NEW_TASK_FORM, {})
|
||||
.then(addNewTask);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return TodoController;
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
<li>
|
||||
<input type="checkbox" class="example-task-checked">
|
||||
<span class="example-task-description">
|
||||
</span>
|
||||
</li>
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="example-todo">
|
||||
<div class="example-button-group">
|
||||
<a class="example-todo-button-all">All</a>
|
||||
<a class="example-todo-button-incomplete">Incomplete</a>
|
||||
<a class="example-todo-button-complete">Complete</a>
|
||||
</div>
|
||||
|
||||
<ul class="example-todo-task-list">
|
||||
</ul>
|
||||
|
||||
<div class="example-message">
|
||||
There are no tasks to show.
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,108 +0,0 @@
|
||||
define([
|
||||
"text!./todo.html",
|
||||
"text!./todo-task.html",
|
||||
"zepto"
|
||||
], function (todoTemplate, taskTemplate, $) {
|
||||
/**
|
||||
* @param {mct.MCT} mct
|
||||
*/
|
||||
return function todoPlugin(mct) {
|
||||
var todoType = new mct.Type({
|
||||
metadata: {
|
||||
label: "To-Do List",
|
||||
glyph: "2",
|
||||
description: "A list of things that need to be done."
|
||||
},
|
||||
initialize: function (model) {
|
||||
model.tasks = [
|
||||
{ description: "This is a task." }
|
||||
];
|
||||
},
|
||||
creatable: true
|
||||
});
|
||||
|
||||
function TodoView(domainObject) {
|
||||
mct.View.apply(this);
|
||||
this.filterValue = "all";
|
||||
this.elements($(todoTemplate));
|
||||
|
||||
var $els = $(this.elements());
|
||||
this.$buttons = {
|
||||
all: $els.find('.example-todo-button-all'),
|
||||
incomplete: $els.find('.example-todo-button-incomplete'),
|
||||
complete: $els.find('.example-todo-button-complete')
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
this.on('model', this.render.bind(this));
|
||||
this.model(domainObject);
|
||||
}
|
||||
|
||||
TodoView.prototype = Object.create(mct.View.prototype);
|
||||
|
||||
TodoView.prototype.setFilter = function (value) {
|
||||
this.filterValue = value;
|
||||
this.render();
|
||||
};
|
||||
|
||||
TodoView.prototype.initialize = function () {
|
||||
Object.keys(this.$buttons).forEach(function (k) {
|
||||
this.$buttons[k].on('click', this.setFilter.bind(this, k));
|
||||
}, this);
|
||||
};
|
||||
|
||||
TodoView.prototype.render = function () {
|
||||
var $els = $(this.elements());
|
||||
var domainObject = this.model();
|
||||
var tasks = domainObject.getModel().tasks;
|
||||
var $message = $els.find('.example-message');
|
||||
var $list = $els.find('.example-todo-task-list');
|
||||
var $buttons = this.$buttons;
|
||||
var filters = {
|
||||
all: function () {
|
||||
return true;
|
||||
},
|
||||
incomplete: function (task) {
|
||||
return !task.completed;
|
||||
},
|
||||
complete: function (task) {
|
||||
return task.completed;
|
||||
}
|
||||
};
|
||||
var filterValue = this.filterValue;
|
||||
|
||||
Object.keys($buttons).forEach(function (k) {
|
||||
$buttons[k].toggleClass('selected', filterValue === k);
|
||||
});
|
||||
tasks = tasks.filter(filters[filterValue]);
|
||||
|
||||
$list.empty();
|
||||
tasks.forEach(function (task, index) {
|
||||
var $taskEls = $(taskTemplate);
|
||||
var $checkbox = $taskEls.find('.example-task-checked');
|
||||
$checkbox.prop('checked', task.completed);
|
||||
$taskEls.find('.example-task-description')
|
||||
.text(task.description);
|
||||
|
||||
$checkbox.on('change', function () {
|
||||
var checked = !!$checkbox.prop('checked');
|
||||
mct.verbs.mutate(domainObject, function (model) {
|
||||
model.tasks[index].completed = checked;
|
||||
});
|
||||
});
|
||||
|
||||
$list.append($taskEls);
|
||||
});
|
||||
|
||||
$message.toggle(tasks.length < 1);
|
||||
};
|
||||
|
||||
todoType.view(mct.regions.main, function (domainObject) {
|
||||
return new TodoView(domainObject);
|
||||
});
|
||||
|
||||
mct.type('example.todo', todoType);
|
||||
|
||||
return mct;
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user