[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

@@ -1,156 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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/representers/EditToolbarRepresenter"],
function (EditToolbarRepresenter) {
describe("The Edit mode toolbar representer", function () {
var mockScope,
mockElement,
testAttrs,
mockUnwatch,
representer,
mockOpenMCT,
mockSelection;
beforeEach(function () {
mockScope = jasmine.createSpyObj(
'$scope',
['$on', '$watch', '$watchCollection', "commit", "$apply"]
);
mockElement = {};
testAttrs = { toolbar: 'testToolbar' };
mockScope.$parent = jasmine.createSpyObj(
'$parent',
['$watch', '$watchCollection']
);
mockUnwatch = jasmine.createSpy('unwatch');
mockScope.$parent.$watchCollection.andReturn(mockUnwatch);
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
representer = new EditToolbarRepresenter(
mockOpenMCT,
mockScope,
mockElement,
testAttrs
);
});
it("exposes toolbar state under a attr-defined name", function () {
// A structure/state object should have been added to the
// parent scope under the name provided in the "toolbar"
// attribute
expect(mockScope.$parent.testToolbar).toBeDefined();
});
it("is robust against lack of a toolbar definition", function () {
expect(function () {
representer.represent({});
}).not.toThrow();
});
it("watches for toolbar state changes", function () {
representer.represent({});
expect(mockScope.$watchCollection).toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Function)
);
expect(mockScope.$watchCollection.calls[0].args[0]())
.toBe(mockScope.$parent.testToolbar.state);
});
it("removes state from parent scope on destroy", function () {
// Verify precondition
expect(mockScope.$parent.testToolbar).toBeDefined();
// Destroy the representer
representer.destroy();
// Should have removed toolbar state from view
expect(mockScope.$parent.testToolbar).toBeUndefined();
});
// Verify a simple interaction between selection state and toolbar
// state; more complicated interactions are tested in EditToolbar.
it("conveys state changes", function () {
var testObject = { k: 123 };
// Provide a view which has a toolbar
representer.represent({
toolbar: { sections: [{ items: [{ property: 'k' }] }] }
});
// Update the selection
mockScope.selection.select(testObject);
expect(mockScope.$watchCollection.mostRecentCall.args[0])
.toEqual('selection.all()'); // Make sure we're using right watch
mockScope.$watchCollection.mostRecentCall.args[1]([testObject]);
// Update the state
mockScope.$parent.testToolbar.state[0] = 456;
// Invoke the first watch (assumed to be for toolbar state)
mockScope.$watchCollection.calls[0].args[1](
mockScope.$parent.testToolbar.state
);
// Should have updated the original object
expect(testObject.k).toEqual(456);
// Should have committed the change
expect(mockScope.commit).toHaveBeenCalled();
});
it("does not commit if nothing changed", function () {
var testObject = { k: 123 };
// Provide a view which has a toolbar
representer.represent({
toolbar: { sections: [{ items: [{ property: 'k' }] }] }
});
// Update the selection
mockScope.selection.select(testObject);
expect(mockScope.$watchCollection.mostRecentCall.args[0])
.toEqual('selection.all()'); // Make sure we're using right watch
mockScope.$watchCollection.mostRecentCall.args[1]([testObject]);
// Invoke the first watch (assumed to be for toolbar state)
mockScope.$watchCollection.calls[0].args[1](
mockScope.$parent.testToolbar.state
);
// Should have committed the change
expect(mockScope.commit).not.toHaveBeenCalled();
});
});
}
);

View File

@@ -1,128 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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/representers/EditToolbarSelection'],
function (EditToolbarSelection) {
describe("The Edit mode selection manager", function () {
var testProxy,
testElement,
otherElement,
selection,
mockSelection,
mockOpenMCT,
mockScope;
beforeEach(function () {
testProxy = { someKey: "some value" };
testElement = { someOtherKey: "some other value" };
otherElement = { yetAnotherKey: 42 };
mockSelection = jasmine.createSpyObj("selection", [
// 'select',
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
mockScope = jasmine.createSpyObj('$scope', [
'$on',
'$apply'
]);
selection = new EditToolbarSelection(mockScope, mockOpenMCT);
selection.proxy(testProxy);
});
it("adds the proxy to the selection array", function () {
expect(selection.all()).toEqual([testProxy]);
});
it("exposes view proxy", function () {
expect(selection.proxy()).toBe(testProxy);
});
it("includes selected objects alongside the proxy", function () {
selection.select(testElement);
expect(selection.all()).toEqual([testProxy, testElement]);
});
it("allows elements to be deselected", function () {
selection.select(testElement);
selection.deselect();
expect(selection.all()).toEqual([testProxy]);
});
it("replaces old selections with new ones", function () {
selection.select(testElement);
selection.select(otherElement);
expect(selection.all()).toEqual([testProxy, otherElement]);
});
it("allows retrieval of the current selection", function () {
selection.select(testElement);
expect(selection.get()).toBe(testElement);
selection.select(otherElement);
expect(selection.get()).toBe(otherElement);
});
it("can check if an element is selected", function () {
selection.select(testElement);
expect(selection.selected(testElement)).toBeTruthy();
expect(selection.selected(otherElement)).toBeFalsy();
selection.select(otherElement);
expect(selection.selected(testElement)).toBeFalsy();
expect(selection.selected(otherElement)).toBeTruthy();
});
it("considers the proxy to be selected", function () {
expect(selection.selected(testProxy)).toBeTruthy();
selection.select(testElement);
// Even when something else is selected...
expect(selection.selected(testProxy)).toBeTruthy();
});
it("treats selection of the proxy as a no-op", function () {
selection.select(testProxy);
expect(selection.all()).toEqual([testProxy]);
});
it("cleans up selection on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.mostRecentCall.args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
});
}
);

View File

@@ -25,7 +25,10 @@ define(
function (EditToolbar) {
describe("An Edit mode toolbar", function () {
var mockCommit,
var mockOpenMCT,
mockScope,
mockObjects,
mockDomainObject,
testStructure,
testAB,
testABC,
@@ -35,35 +38,30 @@ define(
testM,
toolbar;
function getVisibility(obj) {
return !obj.hidden;
}
beforeEach(function () {
mockCommit = jasmine.createSpy('commit');
testStructure = {
sections: [
{
items: [
{ name: "A", property: "a", exclusive: true },
{ name: "B", property: "b", exclusive: true },
{ name: "C", property: "c", exclusive: true }
]
},
{
items: [
{ name: "X", property: "x" },
{ name: "Y", property: "y", exclusive: true },
{ name: "Z", property: "z", exclusive: true }
]
},
{
items: [
{ name: "M", method: "m", exclusive: true }
]
}
]
};
mockOpenMCT = jasmine.createSpy('openmct', ['objects']);
mockObjects = jasmine.createSpyObj('objects', ['observe']);
mockObjects.observe.andReturn();
mockOpenMCT.objects = mockObjects;
mockScope = jasmine.createSpyObj("$scope", [
"$watchCollection",
"$on"
]);
mockScope.$watchCollection.andReturn();
mockDomainObject = jasmine.createSpyObj("domainObject", [
'identifier'
]);
testStructure = [
{ name: "A", property: "a", domainObject: mockDomainObject },
{ name: "B", property: "b", domainObject: mockDomainObject },
{ name: "C", property: "c", domainObject: mockDomainObject },
{ name: "X", property: "x", domainObject: mockDomainObject },
{ name: "Y", property: "y", domainObject: mockDomainObject },
{ name: "Z", property: "z", domainObject: mockDomainObject },
{ name: "M", method: "m", domainObject: mockDomainObject }
];
testAB = { a: 0, b: 1 };
testABC = { a: 0, b: 1, c: 2 };
testABC2 = { a: 4, b: 1, c: 2 }; // For inconsistent-state checking
@@ -71,151 +69,17 @@ define(
testABCYZ = { a: 0, b: 1, c: 2, y: 'Y!', z: 'Z!' };
testM = { m: jasmine.createSpy("method") };
toolbar = new EditToolbar(testStructure, mockCommit);
});
it("provides properties from the original structure", function () {
expect(
new EditToolbar(testStructure, [testABC])
.getStructure()
.sections[0]
.items[1]
.name
).toEqual("B");
});
// This is needed by mct-toolbar
it("adds keys to form structure", function () {
expect(
new EditToolbar(testStructure, [testABC])
.getStructure()
.sections[0]
.items[1]
.key
).not.toBeUndefined();
});
it("marks empty sections as hidden", function () {
// Verify that all sections are included when applicable...
toolbar.setSelection([testABCXYZ]);
expect(toolbar.getStructure().sections.map(getVisibility))
.toEqual([true, true, false]);
// ...but omitted when only some are applicable
toolbar.setSelection([testABC]);
expect(toolbar.getStructure().sections.map(getVisibility))
.toEqual([true, false, false]);
});
it("reads properties from selections", function () {
var structure, state;
toolbar.setSelection([testABC]);
structure = toolbar.getStructure();
state = toolbar.getState();
expect(state[structure.sections[0].items[0].key])
.toEqual(testABC.a);
expect(state[structure.sections[0].items[1].key])
.toEqual(testABC.b);
expect(state[structure.sections[0].items[2].key])
.toEqual(testABC.c);
});
it("reads properties from getters", function () {
var structure, state;
testABC.a = function () {
return "from a getter!";
};
toolbar.setSelection([testABC]);
structure = toolbar.getStructure();
state = toolbar.getState();
expect(state[structure.sections[0].items[0].key])
.toEqual("from a getter!");
});
it("sets properties on update", function () {
toolbar.setSelection([testABC]);
toolbar.updateState(
toolbar.getStructure().sections[0].items[0].key,
"new value"
);
// Should have updated the underlying object
expect(testABC.a).toEqual("new value");
});
it("invokes setters on update", function () {
var structure;
testABC.a = jasmine.createSpy('a');
toolbar.setSelection([testABC]);
structure = toolbar.getStructure();
toolbar.updateState(
structure.sections[0].items[0].key,
"new value"
);
// Should have updated the underlying object
expect(testABC.a).toHaveBeenCalledWith("new value");
});
it("provides a return value describing update status", function () {
// Should return true if actually updated, otherwise false
var key;
toolbar.setSelection([testABC]);
key = toolbar.getStructure().sections[0].items[0].key;
expect(toolbar.updateState(key, testABC.a)).toBeFalsy();
expect(toolbar.updateState(key, "new value")).toBeTruthy();
});
it("removes inapplicable items", function () {
// First, verify with all items
toolbar.setSelection([testABC]);
expect(toolbar.getStructure().sections[0].items.map(getVisibility))
.toEqual([true, true, true]);
// Then, try with some items omitted
toolbar.setSelection([testABC, testAB]);
expect(toolbar.getStructure().sections[0].items.map(getVisibility))
.toEqual([true, true, false]);
});
it("removes inconsistent states", function () {
// Only two of three values match among these selections
toolbar.setSelection([testABC, testABC2]);
expect(toolbar.getStructure().sections[0].items.map(getVisibility))
.toEqual([false, true, true]);
});
it("allows inclusive items", function () {
// One inclusive item is in the set, property 'x' of the
// second section; make sure items are pruned down
// when only some of the selection has x,y,z properties
toolbar.setSelection([testABC, testABCXYZ]);
expect(toolbar.getStructure().sections[1].items.map(getVisibility))
.toEqual([true, false, false]);
});
it("removes inclusive items when there are no matches", function () {
toolbar.setSelection([testABCYZ]);
expect(toolbar.getStructure().sections[1].items.map(getVisibility))
.toEqual([false, true, true]);
toolbar = new EditToolbar(mockScope, mockOpenMCT, testStructure);
});
it("adds click functions when a method is specified", function () {
toolbar.setSelection([testM]);
// Verify precondition
expect(testM.m).not.toHaveBeenCalled();
// Click!
toolbar.getStructure().sections[2].items[0].click();
// Should have called the underlying function
expect(testM.m).toHaveBeenCalled();
// Should also have committed the change
expect(mockCommit).toHaveBeenCalled();
var structure = toolbar.getStructure();
expect(structure[6].click).toBeDefined();
});
it("adds key for controls that define a property", function () {
var structure = toolbar.getStructure();
expect(structure[0].key).toEqual(0);
});
});
}