[Toolbar] Implement a public API for adding toolbars (#1908)

* [API] Implement a toolbar registry and a plugin to allow providing a toolbar for a selected object.
* Modify the mct-toolbar directive to get the toolbar structure from a provider based on selection.
* Implements the layout toolbar in the layout bundle
This commit is contained in:
Pegah Sarram
2018-06-27 13:30:01 -07:00
committed by Andrew Henry
parent de8f8d174d
commit 73e38f1955
39 changed files with 1400 additions and 1844 deletions

View File

@@ -53,22 +53,14 @@ define(
mockTimeSystem,
mockLimitEvaluator,
mockSelection,
mockObjects,
mockNewDomainObject,
unlistenFunc,
$element = [],
selectable = [],
controller;
// Utility function; find a watch for a given expression
function findWatch(expr) {
var watch;
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
watch = call.args[1];
}
});
return watch;
}
// As above, but for $on calls
// Utility function; find a $on calls for a given expression.
function findOn(expr) {
var on;
mockScope.$on.calls.forEach(function (call) {
@@ -82,7 +74,8 @@ define(
function makeMockDomainObject(id) {
return {
identifier: {
key: "domainObject-" + id
key: "domainObject-" + id,
namespace: ""
},
name: "Point " + id
};
@@ -110,11 +103,6 @@ define(
return "Formatted " + valueMetadata.value;
});
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getId', 'getModel', 'getCapability', 'useCapability']
);
mockHandle = jasmine.createSpyObj(
'subscription',
[
@@ -172,16 +160,14 @@ define(
]};
mockChildren = testModel.composition.map(makeMockDomainObject);
mockCompositionCollection = jasmine.createSpyObj('compositionCollection',
[
'load'
]
);
mockCompositionAPI = jasmine.createSpyObj('composition',
[
'get'
]
);
mockCompositionCollection = jasmine.createSpyObj('compositionCollection', [
'load',
'on',
'off'
]);
mockCompositionAPI = jasmine.createSpyObj('composition', [
'get'
]);
mockCompositionAPI.get.andReturn(mockCompositionCollection);
mockCompositionCollection.load.andReturn(
Promise.resolve(mockChildren)
@@ -190,6 +176,24 @@ define(
mockScope.model = testModel;
mockScope.configuration = testConfiguration;
mockNewDomainObject = jasmine.createSpyObj("newDomainObject", [
'layoutGrid',
'configuration',
'composition'
]);
mockNewDomainObject.layoutGrid = testGrid;
mockNewDomainObject.configuration = {
'fixed-display': testConfiguration
};
mockNewDomainObject.composition = ['a', 'b', 'c'];
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getId', 'getModel', 'getCapability', 'useCapability']
);
mockDomainObject.useCapability.andReturn(mockNewDomainObject);
mockScope.domainObject = mockDomainObject;
selectable[0] = {
context: {
oldItem: mockDomainObject
@@ -203,11 +207,19 @@ define(
]);
mockSelection.get.andReturn([]);
unlistenFunc = jasmine.createSpy("unlisten");
mockObjects = jasmine.createSpyObj('objects', [
'observe',
'get'
]);
mockObjects.observe.andReturn(unlistenFunc);
mockOpenMCT = {
time: mockConductor,
telemetry: mockTelemetryAPI,
composition: mockCompositionAPI,
selection: mockSelection
selection: mockSelection,
objects: mockObjects
};
$element = $('<div></div>');
@@ -251,76 +263,60 @@ define(
mockOpenMCT,
$element
);
findWatch("model.layoutGrid")(testModel.layoutGrid);
spyOn(controller, "mutate");
});
it("subscribes when a domain object is available", function () {
var dunzo = false;
it("subscribes a domain object", function () {
var object = makeMockDomainObject("mock");
var done = false;
mockScope.domainObject = mockDomainObject;
findWatch("domainObject")(mockDomainObject).then(function () {
dunzo = true;
controller.getTelemetry(object).then(function () {
done = true;
});
waitsFor(function () {
return dunzo;
}, "Telemetry fetched", 200);
return done;
});
runs(function () {
mockChildren.forEach(function (child) {
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(
child,
jasmine.any(Function),
jasmine.any(Object)
);
});
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(
object,
jasmine.any(Function),
jasmine.any(Object)
);
});
});
it("releases subscriptions when domain objects change", function () {
var dunzo = false;
it("releases subscription when a domain objects is removed", function () {
var done = false;
var unsubscribe = jasmine.createSpy('unsubscribe');
var object = makeMockDomainObject("mock");
mockTelemetryAPI.subscribe.andReturn(unsubscribe);
mockScope.domainObject = mockDomainObject;
findWatch("domainObject")(mockDomainObject).then(function () {
dunzo = true;
controller.getTelemetry(object).then(function () {
done = true;
});
waitsFor(function () {
return dunzo;
}, "Telemetry fetched", 200);
return done;
});
runs(function () {
expect(unsubscribe).not.toHaveBeenCalled();
dunzo = false;
findWatch("domainObject")(mockDomainObject).then(function () {
dunzo = true;
});
controller.onCompositionRemove(object.identifier);
waitsFor(function () {
return dunzo;
}, "Telemetry fetched", 200);
runs(function () {
expect(unsubscribe.calls.length).toBe(mockChildren.length);
return unsubscribe.calls.length > 0;
});
runs(function () {
expect(unsubscribe).toHaveBeenCalled();
});
});
});
it("exposes visible elements based on configuration", function () {
var elements;
var elements = controller.getElements();
mockScope.model = testModel;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
expect(elements.length).toEqual(3);
expect(elements[0].id).toEqual('a');
expect(elements[1].id).toEqual('b');
@@ -328,9 +324,6 @@ define(
});
it("allows elements to be selected", function () {
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@@ -338,12 +331,7 @@ define(
});
it("allows selection retrieval", function () {
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
var elements = controller.getElements();
selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@@ -351,16 +339,10 @@ define(
});
it("selects the parent view when selected element is removed", function () {
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
var elements = controller.getElements();
selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
elements[1].remove();
testModel.modified = 2;
findWatch("model.modified")(testModel.modified);
controller.remove(elements[1]);
expect($element[0].click).toHaveBeenCalled();
});
@@ -368,21 +350,13 @@ define(
it("retains selections during refresh", function () {
// Get elements; remove one of them; trigger refresh.
// Same element (at least by index) should still be selected.
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
var elements = controller.getElements();
selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.getSelectedElement()).toEqual(elements[1]);
elements[2].remove();
testModel.modified = 2;
findWatch("model.modified")(testModel.modified);
controller.remove(elements[2]);
elements = controller.getElements();
// Verify removal, as test assumes this
@@ -408,7 +382,7 @@ define(
controller.elementProxiesById['12345'] = [testElement];
controller.elementProxies = [testElement];
controller.subscribeToObjects([telemetryObject]);
controller.subscribeToObject(telemetryObject);
mockTelemetryAPI.subscribe.mostRecentCall.args[1](mockTelemetry);
waitsFor(function () {
@@ -426,18 +400,13 @@ define(
});
it("updates elements styles when grid size changes", function () {
var originalLeft;
// Grid size is initially set to testGrid which is [123, 456]
var originalLeft = controller.getElements()[0].style.left;
mockScope.domainObject = mockDomainObject;
mockScope.model = testModel;
findWatch("domainObject")(mockDomainObject);
findWatch("model.modified")(1);
findWatch("model.composition")(mockScope.model.composition);
findWatch("model.layoutGrid")([10, 10]);
originalLeft = controller.getElements()[0].style.left;
findWatch("model.layoutGrid")([20, 20]);
expect(controller.getElements()[0].style.left)
.not.toEqual(originalLeft);
// Change the grid size
controller.updateElementPositions([20, 20]);
expect(controller.getElements()[0].style.left).not.toEqual(originalLeft);
});
it("listens for drop events", function () {
@@ -457,6 +426,9 @@ define(
// Notify that a drop occurred
testModel.composition.push('d');
mockObjects.get.andReturn(Promise.resolve([]));
findOn('mctDrop')(
mockEvent,
'd',
@@ -468,11 +440,6 @@ define(
// ...and prevented default...
expect(mockEvent.preventDefault).toHaveBeenCalled();
// Should have triggered commit (provided by
// EditRepresenter) with some message.
expect(mockScope.commit)
.toHaveBeenCalledWith(jasmine.any(String));
});
it("ignores drops when default has been prevented", function () {
@@ -492,52 +459,35 @@ define(
});
it("unsubscribes when destroyed", function () {
var dunzo = false;
var done = false;
var unsubscribe = jasmine.createSpy('unsubscribe');
var object = makeMockDomainObject("mock");
mockTelemetryAPI.subscribe.andReturn(unsubscribe);
mockScope.domainObject = mockDomainObject;
findWatch("domainObject")(mockDomainObject).then(function () {
dunzo = true;
controller.getTelemetry(object).then(function () {
done = true;
});
waitsFor(function () {
return dunzo;
}, "Telemetry fetched", 200);
return done;
});
runs(function () {
expect(unsubscribe).not.toHaveBeenCalled();
// Destroy the scope
findOn('$destroy')();
//Check that the same unsubscribe function returned by the
expect(unsubscribe.calls.length).toBe(mockChildren.length);
expect(unsubscribe).toHaveBeenCalled();
});
});
it("exposes its grid size", function () {
findWatch('model.layoutGrid')(testGrid);
// Template needs to be able to pass this into line
// elements to size SVGs appropriately
expect(controller.getGridSize()).toEqual(testGrid);
});
it("exposes a view-level selection proxy", function () {
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var selection = mockOpenMCT.selection.select.mostRecentCall.args[0];
expect(mockOpenMCT.selection.select).toHaveBeenCalled();
expect(selection.context.viewProxy).toBeDefined();
});
it("exposes drag handles", function () {
var handles;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@@ -556,9 +506,6 @@ define(
});
it("exposes a move handle", function () {
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@@ -573,10 +520,6 @@ define(
it("updates selection style during drag", function () {
var oldStyle;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
@@ -677,7 +620,7 @@ define(
value: testValue
}]));
controller.fetchHistoricalData([mockTelemetryObject]);
controller.fetchHistoricalData(mockTelemetryObject);
waitsFor(function () {
return controller.digesting === false;

View File

@@ -28,8 +28,8 @@ define(
describe("A fixed position drag handle", function () {
var mockElementHandle,
mockUpdate,
mockCommit,
mockConfigPath,
mockFixedControl,
handle;
beforeEach(function () {
@@ -37,18 +37,23 @@ define(
'elementHandle',
['x', 'y','getGridSize']
);
mockUpdate = jasmine.createSpy('update');
mockCommit = jasmine.createSpy('commit');
mockElementHandle.x.andReturn(6);
mockElementHandle.y.andReturn(8);
mockElementHandle.getGridSize.andReturn(TEST_GRID_SIZE);
mockFixedControl = jasmine.createSpyObj(
'fixedControl',
['updateSelectionStyle', 'mutate']
);
mockFixedControl.updateSelectionStyle.andReturn();
mockFixedControl.mutate.andReturn();
mockConfigPath = jasmine.createSpy('configPath');
handle = new FixedDragHandle(
mockElementHandle,
TEST_GRID_SIZE,
mockUpdate,
mockCommit
mockConfigPath,
mockFixedControl
);
});
@@ -74,13 +79,12 @@ define(
expect(mockElementHandle.x).toHaveBeenCalledWith(5);
expect(mockElementHandle.y).toHaveBeenCalledWith(7);
// Should have called update once per continueDrag
expect(mockUpdate.calls.length).toEqual(2);
// Should have called updateSelectionStyle once per continueDrag
expect(mockFixedControl.updateSelectionStyle.calls.length).toEqual(2);
// Finally, ending drag should commit
expect(mockCommit).not.toHaveBeenCalled();
// Finally, ending drag should mutate
handle.endDrag();
expect(mockCommit).toHaveBeenCalled();
expect(mockFixedControl.mutate).toHaveBeenCalled();
});
});

View File

@@ -42,6 +42,8 @@ define(
mockOpenMCT,
mockSelection,
mockDomainObjectCapability,
mockObjects,
unlistenFunc,
$element = [],
selectable = [];
@@ -77,14 +79,15 @@ define(
if (param === 'composition') {
return id !== 'b';
}
}
},
type: "testType"
};
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
["$watch", "$watchCollection", "$on", "commit"]
["$watch", "$watchCollection", "$on"]
);
mockEvent = jasmine.createSpyObj(
'event',
@@ -104,9 +107,13 @@ define(
}
}
};
unlistenFunc = jasmine.createSpy("unlisten");
mockDomainObjectCapability = jasmine.createSpyObj('capability',
['inEditContext']
['inEditContext', 'listen']
);
mockDomainObjectCapability.listen.andReturn(unlistenFunc);
mockCompositionCapability = mockPromise(mockCompositionObjects);
mockScope.domainObject = mockDomainObject("mockDomainObject");
@@ -126,8 +133,14 @@ define(
'get'
]);
mockSelection.get.andReturn(selectable);
mockObjects = jasmine.createSpyObj('objects', [
'get'
]);
mockObjects.get.andReturn(mockPromise(mockDomainObject("mockObject")));
mockOpenMCT = {
selection: mockSelection
selection: mockSelection,
objects: mockObjects
};
$element = $('<div></div>');
@@ -138,6 +151,7 @@ define(
controller = new LayoutController(mockScope, $element, mockOpenMCT);
spyOn(controller, "layoutPanels").andCallThrough();
spyOn(controller, "commit");
jasmine.Clock.useMock();
});
@@ -270,10 +284,7 @@ define(
controller.continueDrag([100, 100]);
controller.endDrag();
// Should have triggered commit (provided by
// EditRepresenter) with some message.
expect(mockScope.commit)
.toHaveBeenCalledWith(jasmine.any(String));
expect(controller.commit).toHaveBeenCalled();
});
it("listens for drop events", function () {
@@ -296,11 +307,7 @@ define(
);
expect(testConfiguration.panels.d).toBeDefined();
expect(mockEvent.preventDefault).toHaveBeenCalled();
// Should have triggered commit (provided by
// EditRepresenter) with some message.
expect(mockScope.commit)
.toHaveBeenCalledWith(jasmine.any(String));
expect(controller.commit).toHaveBeenCalled();
});
it("ignores drops when default has been prevented", function () {
@@ -340,13 +347,17 @@ define(
testModel.layoutGrid = [1, 1];
mockScope.$watch.calls[0].args[1](testModel.layoutGrid);
// Add a new object to the composition
mockComposition = ["a", "b", "c", "d"];
mockCompositionObjects = mockComposition.map(mockDomainObject);
mockCompositionCapability = mockPromise(mockCompositionObjects);
// Notify that a drop occurred
mockScope.$on.mostRecentCall.args[1](
mockEvent,
'd',
{ x: 300, y: 100 }
);
mockScope.$watch.calls[0].args[1](['d']);
style = controller.getFrameStyle("d");
@@ -415,30 +426,6 @@ define(
expect(controller.hasFrame(mockCompositionObjects[1])).toBe(false);
});
it("hides frame when selected object has frame ", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
expect(controller.hasFrame(childObj)).toBe(true);
expect(toolbarObj.hideFrame).toBeDefined();
expect(toolbarObj.hideFrame).toEqual(jasmine.any(Function));
});
it("shows frame when selected object has no frame", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[1];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
expect(controller.hasFrame(childObj)).toBe(false);
expect(toolbarObj.showFrame).toBeDefined();
expect(toolbarObj.showFrame).toEqual(jasmine.any(Function));
});
it("selects the parent object when selected object is removed", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];

View File

@@ -53,11 +53,6 @@ define(
});
});
it("allows elements to be removed", function () {
proxy.remove();
expect(testElements).toEqual([{}, {}, {}]);
});
it("allows order to be changed", function () {
proxy.order("down");
expect(testElements).toEqual([{}, testElement, {}, {}]);

View File

@@ -26,7 +26,9 @@ define(
describe("A fixed position drag handle", function () {
var testElement,
handle;
mockElementProxy,
handle,
TEST_GRID_SIZE = [45, 21];
beforeEach(function () {
testElement = {
@@ -36,8 +38,10 @@ define(
y2: 11,
useGrid: true
};
mockElementProxy = jasmine.createSpyObj('elementProxy', ['getGridSize']);
mockElementProxy.getGridSize.andReturn(TEST_GRID_SIZE);
handle = new LineHandle(testElement, 'x', 'y', 'x2', 'y2', [45,21]);
handle = new LineHandle(testElement, mockElementProxy, 'x', 'y', 'x2', 'y2');
});
it("provides x/y grid coordinates for its corner", function () {
@@ -69,7 +73,7 @@ define(
});
it("returns the correct grid size", function () {
expect(handle.getGridSize()).toEqual([45,21]);
expect(handle.getGridSize()).toEqual(TEST_GRID_SIZE);
});
});

View File

@@ -63,13 +63,13 @@ define(
it("adjusts both ends when mutating x", function () {
var proxy = new LineProxy(diagonal);
proxy.x(6);
expect(diagonal).toEqual({ x: 6, y: 8, x2: 8, y2: 11, useGrid: true });
expect(diagonal).toEqual({ x: 6, y: 8, x2: 8, y2: 11});
});
it("adjusts both ends when mutating y", function () {
var proxy = new LineProxy(diagonal);
proxy.y(6);
expect(diagonal).toEqual({ x: 3, y: 6, x2: 5, y2: 9, useGrid: true });
expect(diagonal).toEqual({ x: 3, y: 6, x2: 5, y2: 9});
});
it("provides internal positions for SVG lines", function () {

View File

@@ -25,10 +25,12 @@ define(
function (ResizeHandle) {
var TEST_MIN_WIDTH = 4,
TEST_MIN_HEIGHT = 2;
TEST_MIN_HEIGHT = 2,
TEST_GRID_SIZE = [34, 81];
describe("A fixed position drag handle", function () {
var testElement,
mockElementProxy,
handle;
beforeEach(function () {
@@ -39,12 +41,18 @@ define(
height: 36,
useGrid: true
};
mockElementProxy = jasmine.createSpyObj('elementProxy', [
'getGridSize',
'getMinWidth',
'getMinHeight'
]);
mockElementProxy.getGridSize.andReturn(TEST_GRID_SIZE);
mockElementProxy.getMinWidth.andReturn(TEST_MIN_WIDTH);
mockElementProxy.getMinHeight.andReturn(TEST_MIN_HEIGHT);
handle = new ResizeHandle(
testElement,
TEST_MIN_WIDTH,
TEST_MIN_HEIGHT,
[34,81]
mockElementProxy,
testElement
);
});
@@ -77,7 +85,7 @@ define(
});
it("returns the correct grid size", function () {
expect(handle.getGridSize()).toEqual([34,81]);
expect(handle.getGridSize()).toEqual(TEST_GRID_SIZE);
});
});

View File

@@ -49,27 +49,6 @@ define(
it("exposes the element's id", function () {
expect(proxy.id).toEqual('test-id');
});
it("allows title to be shown/hidden", function () {
// Initially, only showTitle and hideTitle are available
expect(proxy.hideTitle).toBeUndefined();
proxy.showTitle();
// Should have set titled state
expect(testElement.titled).toBeTruthy();
// Should also have changed methods available
expect(proxy.showTitle).toBeUndefined();
proxy.hideTitle();
// Should have cleared titled state
expect(testElement.titled).toBeFalsy();
// Available methods should have changed again
expect(proxy.hideTitle).toBeUndefined();
proxy.showTitle();
});
});
}
);