Merge branch 'master' into open199

This commit is contained in:
Henry 2015-11-19 14:29:32 -08:00
commit 30e6980dc6
14 changed files with 216 additions and 51 deletions

View File

@ -62,7 +62,7 @@ define(
// so it's not checked for here, just formatted for display // so it's not checked for here, just formatted for display
// differently. // differently.
return (i + offset) * 1000 + firstTime * 1000 - return (i + offset) * 1000 + firstTime * 1000 -
(domain === 'yesterday' ? ONE_DAY : 0); (domain === 'yesterday' ? (ONE_DAY * 1000) : 0);
}; };
generatorData.getRangeValue = function (i, range) { generatorData.getRangeValue = function (i, range) {

View File

@ -97,7 +97,7 @@
"provides": "actionService", "provides": "actionService",
"type": "provider", "type": "provider",
"implementation": "actions/ActionProvider.js", "implementation": "actions/ActionProvider.js",
"depends": [ "actions[]" ] "depends": [ "actions[]", "$log" ]
}, },
{ {
"provides": "actionService", "provides": "actionService",

View File

@ -39,9 +39,11 @@ define(
* @imeplements {ActionService} * @imeplements {ActionService}
* @constructor * @constructor
*/ */
function ActionProvider(actions) { function ActionProvider(actions, $log) {
var self = this; var self = this;
this.$log = $log;
// Build up look-up tables // Build up look-up tables
this.actions = actions; this.actions = actions;
this.actionsByKey = {}; this.actionsByKey = {};
@ -74,6 +76,7 @@ define(
var context = (actionContext || {}), var context = (actionContext || {}),
category = context.category, category = context.category,
key = context.key, key = context.key,
$log = this.$log,
candidates; candidates;
// Instantiate an action; invokes the constructor and // Instantiate an action; invokes the constructor and
@ -103,12 +106,31 @@ define(
// appliesTo method of given actions (if defined), and // appliesTo method of given actions (if defined), and
// instantiate those applicable actions. // instantiate those applicable actions.
function createIfApplicable(actions, context) { function createIfApplicable(actions, context) {
return (actions || []).filter(function (Action) { function isApplicable(Action) {
return Action.appliesTo ? return Action.appliesTo ? Action.appliesTo(context) : true;
Action.appliesTo(context) : true; }
}).map(function (Action) {
function instantiate(Action) {
try {
return instantiateAction(Action, context); return instantiateAction(Action, context);
}); } catch (e) {
$log.error([
"Could not instantiate action",
Action.key,
"due to:",
e.message
].join(" "));
return undefined;
}
}
function isDefined(action) {
return action !== undefined;
}
return (actions || []).filter(isApplicable)
.map(instantiate)
.filter(isDefined);
} }
// Match actions to the provided context by comparing "key" // Match actions to the provided context by comparing "key"

View File

@ -156,6 +156,13 @@ define(
}); });
}; };
/**
* Returns the default model for an object of this type. Note that
* this method returns a clone of the original model, so if using this
* method heavily, consider caching the result to optimize performance.
*
* @return {object} The default model for an object of this type.
*/
TypeImpl.prototype.getInitialModel = function () { TypeImpl.prototype.getInitialModel = function () {
return JSON.parse(JSON.stringify(this.typeDef.model || {})); return JSON.parse(JSON.stringify(this.typeDef.model || {}));
}; };

View File

@ -30,7 +30,8 @@ define(
"use strict"; "use strict";
describe("The action provider", function () { describe("The action provider", function () {
var actions, var mockLog,
actions,
actionProvider; actionProvider;
function SimpleAction() { function SimpleAction() {
@ -62,6 +63,10 @@ define(
MetadataAction.key = "metadata"; MetadataAction.key = "metadata";
beforeEach(function () { beforeEach(function () {
mockLog = jasmine.createSpyObj(
'$log',
['error', 'warn', 'info', 'debug']
);
actions = [ actions = [
SimpleAction, SimpleAction,
CategorizedAction, CategorizedAction,
@ -137,6 +142,42 @@ define(
expect(provided[0].getMetadata()).toEqual("custom metadata"); expect(provided[0].getMetadata()).toEqual("custom metadata");
}); });
describe("when actions throw errors during instantiation", function () {
var errorText,
provided;
beforeEach(function () {
errorText = "some error text";
function BadAction() {
throw new Error(errorText);
}
provided = new ActionProvider(
[ SimpleAction, BadAction ],
mockLog
).getActions();
});
it("logs an error", function () {
expect(mockLog.error)
.toHaveBeenCalledWith(jasmine.any(String));
});
it("reports the error's message", function () {
expect(
mockLog.error.mostRecentCall.args[0].indexOf(errorText)
).not.toEqual(-1);
});
it("still provides valid actions", function () {
expect(provided.length).toEqual(1);
expect(provided[0].perform()).toEqual("simple");
});
});
}); });
} }
); );

View File

@ -102,6 +102,8 @@ define(
}); });
it("provides a fresh initial model each time", function () { it("provides a fresh initial model each time", function () {
var model = type.getInitialModel();
model.someKey = "some other value";
expect(type.getInitialModel().someKey).toEqual("some value"); expect(type.getInitialModel().someKey).toEqual("some value");
}); });

View File

@ -122,6 +122,14 @@ define(
}); });
}; };
AbstractComposeAction.appliesTo = function (context) {
var applicableObject =
context.selectedObject || context.domainObject;
return !!(applicableObject &&
applicableObject.hasCapability('context'));
};
return AbstractComposeAction; return AbstractComposeAction;
} }
); );

View File

@ -131,6 +131,9 @@ define(
return AbstractComposeAction.prototype.perform.call(this) return AbstractComposeAction.prototype.perform.call(this)
.then(success, error, notification); .then(success, error, notification);
}; };
CopyAction.appliesTo = AbstractComposeAction.appliesTo;
return CopyAction; return CopyAction;
} }
); );

View File

@ -35,14 +35,15 @@ define(
* @memberof platform/entanglement * @memberof platform/entanglement
*/ */
function LinkAction(locationService, linkService, context) { function LinkAction(locationService, linkService, context) {
return new AbstractComposeAction( AbstractComposeAction.apply(
locationService, this,
linkService, [locationService, linkService, context, "Link"]
context,
"Link"
); );
} }
LinkAction.prototype = Object.create(AbstractComposeAction.prototype);
LinkAction.appliesTo = AbstractComposeAction.appliesTo;
return LinkAction; return LinkAction;
} }
); );

View File

@ -35,14 +35,16 @@ define(
* @memberof platform/entanglement * @memberof platform/entanglement
*/ */
function MoveAction(locationService, moveService, context) { function MoveAction(locationService, moveService, context) {
return new AbstractComposeAction( AbstractComposeAction.apply(
locationService, this,
moveService, [locationService, moveService, context, "Move"]
context,
"Move"
); );
} }
MoveAction.prototype = Object.create(AbstractComposeAction.prototype);
MoveAction.appliesTo = AbstractComposeAction.appliesTo;
return MoveAction; return MoveAction;
} }
); );

View File

@ -94,6 +94,28 @@ define(
composeService = new MockCopyService(); composeService = new MockCopyService();
}); });
it("are only applicable to domain objects with a context", function () {
var noContextObject = domainObjectFactory({
name: 'selectedObject',
model: { name: 'selectedObject' },
capabilities: {}
});
expect(AbstractComposeAction.appliesTo({
selectedObject: selectedObject
})).toBe(true);
expect(AbstractComposeAction.appliesTo({
domainObject: selectedObject
})).toBe(true);
expect(AbstractComposeAction.appliesTo({
selectedObject: noContextObject
})).toBe(false);
expect(AbstractComposeAction.appliesTo({
domainObject: noContextObject
})).toBe(false);
});
describe("with context from context-action", function () { describe("with context from context-action", function () {
beforeEach(function () { beforeEach(function () {

View File

@ -78,6 +78,8 @@ define(
cachedObjects = [], cachedObjects = [],
updater, updater,
lastBounds, lastBounds,
lastRange,
lastDomain,
handle; handle;
// Populate the scope with axis information (specifically, options // Populate the scope with axis information (specifically, options
@ -120,16 +122,16 @@ define(
// Reinstantiate the plot updater (e.g. because we have a // Reinstantiate the plot updater (e.g. because we have a
// new subscription.) This will clear the plot. // new subscription.) This will clear the plot.
function recreateUpdater() { function recreateUpdater() {
updater = new PlotUpdater( var domain = $scope.axes[0].active.key,
handle, range = $scope.axes[1].active.key,
($scope.axes[0].active || {}).key, duration = PLOT_FIXED_DURATION;
($scope.axes[1].active || {}).key,
PLOT_FIXED_DURATION updater = new PlotUpdater(handle, domain, range, duration);
); lastDomain = domain;
self.limitTracker = new PlotLimitTracker( lastRange = range;
handle,
($scope.axes[1].active || {}).key self.limitTracker = new PlotLimitTracker(handle, range);
);
// Keep any externally-provided bounds // Keep any externally-provided bounds
if (lastBounds) { if (lastBounds) {
setBasePanZoom(lastBounds); setBasePanZoom(lastBounds);
@ -201,22 +203,39 @@ define(
} }
} }
function requery() {
self.pending = true;
releaseSubscription();
subscribe($scope.domainObject);
}
function updateDomainFormat() {
var domainAxis = $scope.axes[0];
plotTelemetryFormatter
.setDomainFormat(domainAxis.active.format);
}
function domainRequery(newDomain) {
if (newDomain !== lastDomain) {
updateDomainFormat();
requery();
}
}
function rangeRequery(newRange) {
if (newRange !== lastRange) {
requery();
}
}
// Respond to a display bounds change (requery for data) // Respond to a display bounds change (requery for data)
function changeDisplayBounds(event, bounds) { function changeDisplayBounds(event, bounds) {
var domainAxis = $scope.axes[0]; var domainAxis = $scope.axes[0];
domainAxis.chooseOption(bounds.domain); domainAxis.chooseOption(bounds.domain);
plotTelemetryFormatter updateDomainFormat();
.setDomainFormat(domainAxis.active.format);
self.pending = true;
releaseSubscription();
subscribe($scope.domainObject);
setBasePanZoom(bounds); setBasePanZoom(bounds);
} requery();
function updateDomainFormat(format) {
plotTelemetryFormatter.setDomainFormat(format);
} }
this.modeOptions = new PlotModeOptions([], subPlotFactory); this.modeOptions = new PlotModeOptions([], subPlotFactory);
@ -237,6 +256,10 @@ define(
new PlotAxis("ranges", [], AXIS_DEFAULTS[1]) new PlotAxis("ranges", [], AXIS_DEFAULTS[1])
]; ];
// Watch for changes to the selected axis
$scope.$watch("axes[0].active.key", domainRequery);
$scope.$watch("axes[1].active.key", rangeRequery);
// Subscribe to telemetry when a domain object becomes available // Subscribe to telemetry when a domain object becomes available
$scope.$watch('domainObject', subscribe); $scope.$watch('domainObject', subscribe);

View File

@ -53,6 +53,14 @@ define(
}); });
} }
function fireWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1].apply(null, [value]);
}
});
}
beforeEach(function () { beforeEach(function () {
mockScope = jasmine.createSpyObj( mockScope = jasmine.createSpyObj(
@ -263,6 +271,20 @@ define(
]); ]);
expect(mockHandle.request.calls.length).toEqual(2); expect(mockHandle.request.calls.length).toEqual(2);
}); });
it("requeries when user changes domain selection", function () {
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockHandle.request.calls.length).toEqual(1);
fireWatch("axes[0].active.key", 'someNewKey');
expect(mockHandle.request.calls.length).toEqual(2);
});
it("requeries when user changes range selection", function () {
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockHandle.request.calls.length).toEqual(1);
fireWatch("axes[1].active.key", 'someNewKey');
expect(mockHandle.request.calls.length).toEqual(2);
});
}); });
} }
); );

View File

@ -285,11 +285,17 @@ define(
* domain objects returned by `getTelemetryObjects()`. * domain objects returned by `getTelemetryObjects()`.
* *
* @param {DomainObject} domainObject the object of interest * @param {DomainObject} domainObject the object of interest
* @param {string} [key] the symbolic identifier of the domain
* to look up; if omitted, the value for this object's
* default domain will be used
* @returns the most recent domain value observed * @returns the most recent domain value observed
*/ */
TelemetrySubscription.prototype.getDomainValue = function (domainObject) { TelemetrySubscription.prototype.getDomainValue = function (domainObject, key) {
var id = domainObject.getId(); var id = domainObject.getId(),
return (this.latestValues[id] || {}).domain; latestValue = this.latestValues[id];
return latestValue && (key ?
latestValue.datum[key] :
latestValue.domain);
}; };
/** /**
@ -302,11 +308,17 @@ define(
* domain objects returned by `getTelemetryObjects()`. * domain objects returned by `getTelemetryObjects()`.
* *
* @param {DomainObject} domainObject the object of interest * @param {DomainObject} domainObject the object of interest
* @param {string} [key] the symbolic identifier of the range
* to look up; if omitted, the value for this object's
* default range will be used
* @returns the most recent range value observed * @returns the most recent range value observed
*/ */
TelemetrySubscription.prototype.getRangeValue = function (domainObject) { TelemetrySubscription.prototype.getRangeValue = function (domainObject, key) {
var id = domainObject.getId(); var id = domainObject.getId(),
return (this.latestValues[id] || {}).range; latestValue = this.latestValues[id];
return latestValue && (key ?
latestValue.datum[key] :
latestValue.range);
}; };
/** /**