mirror of
https://github.com/nasa/openmct.git
synced 2025-02-21 01:42:31 +00:00
Merge remote-tracking branch 'origin/open878' into open-master
Conflicts: platform/commonUI/edit/res/templates/edit-object.html
This commit is contained in:
commit
a3685d0c6b
@ -1 +1,41 @@
|
||||
Contains sources and resources associated with Edit mode.
|
||||
Contains sources and resources associated with Edit mode.
|
||||
|
||||
# Toolbars
|
||||
|
||||
Views may specify the contents of a toolbar through a `toolbar`
|
||||
property in their bundle definition. This should appear as the
|
||||
structure one would provide to the `mct-toolbar` directive,
|
||||
except additional properties are recognized to support the
|
||||
mediation between toolbar contents, user interaction, and the
|
||||
current selection (as read from the `selection` property of the
|
||||
view's scope.) These additional properties are:
|
||||
|
||||
* `property`: Name of the property within a selected object. If,
|
||||
for any given object in the selection, that field is a function,
|
||||
then that function is assumed to be an accessor-mutator function
|
||||
(that is, it will be called with no arguments to get, and with
|
||||
an argument to set.)
|
||||
* `inclusive`: Optional; true if this control should be considered
|
||||
applicable whenever at least one element in the selection has
|
||||
the associated property. Otherwise, all members of the current
|
||||
selection must have this property for the control to be shown.
|
||||
|
||||
Controls in the toolbar are shown based on applicability to the
|
||||
current selection. Applicability for a given member of the selection
|
||||
is determined by the presence of absence of the named `property`
|
||||
field. As a consequence of this, if `undefined` is a valid value for
|
||||
that property, an accessor-mutator function must be used. Likewise,
|
||||
if toolbar properties are meant to be view-global (as opposed to
|
||||
per-selection) then the view must include some object to act as its
|
||||
proxy in the current selection (in addition to whatever objects the
|
||||
user will conceive of as part of the current selection), typically
|
||||
with `inclusive` set to `true`.
|
||||
|
||||
## Selection
|
||||
|
||||
The `selection` property of a view's scope in Edit mode will be
|
||||
initialized to an empty array. This array's contents may be modified
|
||||
to implicitly change the contents of the toolbar based on the rules
|
||||
described above. Care should be taken to modify this array in-place
|
||||
instead of shadowing it (as the selection will typically
|
||||
be a few scopes up the hierarchy from the view's actual scope.)
|
||||
|
@ -94,6 +94,9 @@
|
||||
{
|
||||
"implementation": "EditRepresenter.js",
|
||||
"depends": [ "$q", "$log" ]
|
||||
},
|
||||
{
|
||||
"implementation": "representers/EditToolbarRepresenter.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -2,20 +2,17 @@
|
||||
mct-object="domainObject"
|
||||
ng-model="representation">
|
||||
</mct-representation>
|
||||
<div class="holder edit-area outline abs"
|
||||
ng-init="toolbar = {}">
|
||||
|
||||
<div class="holder edit-area outline abs">
|
||||
<mct-toolbar name="mctToolbar"
|
||||
structure="toolbar.structure"
|
||||
ng-model="toolbar.state">
|
||||
</mct-toolbar>
|
||||
|
||||
<div class='split-layout vertical contents abs work-area'>
|
||||
<div class='abs pane left edit-main'>
|
||||
<div class='holder abs object-holder'>
|
||||
<mct-representation key="representation.selected.key"
|
||||
toolbar="toolbar"
|
||||
mct-object="representation.selected.key && domainObject">
|
||||
mct-object="domainObject">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
||||
|
170
platform/commonUI/edit/src/representers/EditToolbar.js
Normal file
170
platform/commonUI/edit/src/representers/EditToolbar.js
Normal file
@ -0,0 +1,170 @@
|
||||
/*global define*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
// Utility functions for reducing truth arrays
|
||||
function and(a, b) { return a && b; }
|
||||
function or(a, b) { return a || b; }
|
||||
|
||||
|
||||
/**
|
||||
* Provides initial structure and state (as suitable for provision
|
||||
* to the `mct-toolbar` directive) for a view's tool bar, based on
|
||||
* that view's declaration of what belongs in its tool bar and on
|
||||
* the current selection.
|
||||
*
|
||||
* @param structure toolbar structure, as provided by view definition
|
||||
* @param {Array} selection the current selection state
|
||||
* @constructor
|
||||
*/
|
||||
function EditToolbar(structure, selection) {
|
||||
var toolbarStructure = Object.create(structure || {}),
|
||||
toolbarState,
|
||||
properties = [];
|
||||
|
||||
// Generate a new key for an item's property
|
||||
function addKey(property) {
|
||||
properties.push(property);
|
||||
return properties.length - 1; // Return index of property
|
||||
}
|
||||
|
||||
// Update value for this property in all elements of the
|
||||
// selection which have this property.
|
||||
function updateProperties(property, value) {
|
||||
// Update property in a selected element
|
||||
function updateProperty(selected) {
|
||||
// Ignore selected elements which don't have this property
|
||||
if (selected[property] !== undefined) {
|
||||
// Check if this is a setter, or just assignable
|
||||
if (typeof selected[property] === 'function') {
|
||||
selected[property](value);
|
||||
} else {
|
||||
selected[property] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update property in all selected elements
|
||||
selection.forEach(updateProperty);
|
||||
}
|
||||
|
||||
// Look up the current value associated with a property
|
||||
// in selection i
|
||||
function lookupState(property, selected) {
|
||||
var value = selected[property];
|
||||
return (typeof value === 'function') ? value() : value;
|
||||
}
|
||||
|
||||
// Get initial value for a given property
|
||||
function initializeState(property) {
|
||||
var result;
|
||||
// Look through all selections for this property;
|
||||
// values should all match by the time we perform
|
||||
// this lookup anyway.
|
||||
selection.forEach(function (selected) {
|
||||
result = (selected[property] !== undefined) ?
|
||||
lookupState(property, selected) :
|
||||
result;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if all elements of the selection which have this
|
||||
// property have the same value for this property.
|
||||
function isConsistent(property) {
|
||||
var consistent = true,
|
||||
observed = false,
|
||||
state;
|
||||
|
||||
// Check if a given element of the selection is consistent
|
||||
// with previously-observed elements for this property.
|
||||
function checkConsistency(selected) {
|
||||
var next;
|
||||
// Ignore selections which don't have this property
|
||||
if (selected[property] !== undefined) {
|
||||
// Look up state of this element in the selection
|
||||
next = lookupState(property, selected);
|
||||
// Detect inconsistency
|
||||
if (observed) {
|
||||
consistent = consistent && (next === state);
|
||||
}
|
||||
// Track state for next iteration
|
||||
state = next;
|
||||
observed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through selections
|
||||
selection.forEach(checkConsistency);
|
||||
|
||||
return consistent;
|
||||
}
|
||||
|
||||
// Used to filter out items which are applicable (or not)
|
||||
// to the current selection.
|
||||
function isApplicable(item) {
|
||||
var property = (item || {}).property,
|
||||
exclusive = !(item || {}).inclusive;
|
||||
|
||||
// Check if a selected item defines this property
|
||||
function hasProperty(selected) {
|
||||
return selected[property] !== undefined;
|
||||
}
|
||||
|
||||
return property && selection.map(hasProperty).reduce(
|
||||
exclusive ? and : or,
|
||||
exclusive
|
||||
) && isConsistent(property);
|
||||
}
|
||||
|
||||
// Prepare a toolbar item based on current selection
|
||||
function convertItem(item) {
|
||||
var converted = Object.create(item || {});
|
||||
converted.key = addKey(item.property);
|
||||
return converted;
|
||||
}
|
||||
|
||||
// Used to filter out sections that have become empty
|
||||
function nonEmpty(section) {
|
||||
return section && section.items && section.items.length > 0;
|
||||
}
|
||||
|
||||
// Prepare a toolbar section based on current selection
|
||||
function convertSection(section) {
|
||||
var converted = Object.create(section || {});
|
||||
converted.items =
|
||||
((section || {}).items || [])
|
||||
.map(convertItem)
|
||||
.filter(isApplicable);
|
||||
return converted;
|
||||
}
|
||||
|
||||
toolbarStructure.sections =
|
||||
((structure || {}).sections || [])
|
||||
.map(convertSection)
|
||||
.filter(nonEmpty);
|
||||
|
||||
toolbarState = properties.map(initializeState);
|
||||
|
||||
return {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
getStructure: function () {
|
||||
return toolbarStructure;
|
||||
},
|
||||
getState: function () {
|
||||
return toolbarState;
|
||||
},
|
||||
updateState: function (key, value) {
|
||||
updateProperties(properties[key], value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return EditToolbar;
|
||||
}
|
||||
);
|
||||
|
@ -0,0 +1,97 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
['./EditToolbar'],
|
||||
function (EditToolbar) {
|
||||
"use strict";
|
||||
|
||||
// No operation
|
||||
function noop() {}
|
||||
|
||||
/**
|
||||
* The EditToolbarRepresenter populates the toolbar in Edit mode
|
||||
* based on a view's definition.
|
||||
* @param {Scope} scope the Angular scope of the representation
|
||||
* @constructor
|
||||
*/
|
||||
function EditToolbarRepresenter(scope, element, attrs) {
|
||||
var definition,
|
||||
unwatch,
|
||||
toolbar,
|
||||
toolbarObject = {};
|
||||
|
||||
// Handle changes to the current selection
|
||||
function updateSelection(selection) {
|
||||
// Make sure selection is array-like
|
||||
selection = Array.isArray(selection) ?
|
||||
selection :
|
||||
(selection ? [selection] : []);
|
||||
|
||||
// Instantiate a new toolbar...
|
||||
toolbar = new EditToolbar(definition, selection);
|
||||
|
||||
// ...and expose its structure/state
|
||||
toolbarObject.structure = toolbar.getStructure();
|
||||
toolbarObject.state = toolbar.getState();
|
||||
}
|
||||
|
||||
// Update selection models to match changed toolbar state
|
||||
function updateState(state) {
|
||||
state.forEach(function (value, index) {
|
||||
toolbar.updateState(index, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Represent a domain object using this definition
|
||||
function represent(representation) {
|
||||
// Clear any existing selection
|
||||
scope.selection = [];
|
||||
// Get the newest toolbar definition from the view
|
||||
definition = (representation || {}).toolbar || {};
|
||||
// Initialize toolbar to an empty selection
|
||||
updateSelection([]);
|
||||
}
|
||||
|
||||
// Destroy; stop watching the parent for changes in
|
||||
// toolbar state.
|
||||
function destroy() {
|
||||
if (unwatch) {
|
||||
unwatch();
|
||||
unwatch = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have been asked to expose toolbar state...
|
||||
if (attrs.toolbar) {
|
||||
// Expose toolbar state under that name
|
||||
scope.$parent[attrs.toolbar] = toolbarObject;
|
||||
// Detect and handle changes to state from the toolbar
|
||||
unwatch = scope.$parent.$watchCollection(
|
||||
attrs.toolbar + ".state",
|
||||
updateState
|
||||
);
|
||||
// Watch for changes in the current selection state
|
||||
scope.$watchCollection("selection", updateSelection);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Set the current representation in use, and the domain
|
||||
* object being represented.
|
||||
*
|
||||
* @param {RepresentationDefinition} representation the
|
||||
* definition of the representation in use
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* being represented
|
||||
*/
|
||||
represent: (attrs || {}).toolbar ? represent : noop,
|
||||
/**
|
||||
* Release any resources associated with this representer.
|
||||
*/
|
||||
destroy: (attrs || {}).toolbar ? destroy : noop
|
||||
};
|
||||
}
|
||||
|
||||
return EditToolbarRepresenter;
|
||||
}
|
||||
);
|
@ -0,0 +1,91 @@
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/representers/EditToolbarRepresenter"],
|
||||
function (EditToolbarRepresenter) {
|
||||
"use strict";
|
||||
|
||||
describe("The Edit mode toolbar representer", function () {
|
||||
var mockScope,
|
||||
mockElement,
|
||||
testAttrs,
|
||||
mockUnwatch,
|
||||
representer;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
'$scope',
|
||||
[ '$on', '$watch', '$watchCollection' ]
|
||||
);
|
||||
mockElement = {};
|
||||
testAttrs = { toolbar: 'testToolbar' };
|
||||
mockScope.$parent = jasmine.createSpyObj(
|
||||
'$parent',
|
||||
[ '$watch', '$watchCollection' ]
|
||||
);
|
||||
mockUnwatch = jasmine.createSpy('unwatch');
|
||||
|
||||
mockScope.$parent.$watchCollection.andReturn(mockUnwatch);
|
||||
|
||||
representer = new EditToolbarRepresenter(
|
||||
mockScope,
|
||||
mockElement,
|
||||
testAttrs
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes toolbar state under a attr-defined name", function () {
|
||||
// A strucutre/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 () {
|
||||
expect(mockScope.$parent.$watchCollection).toHaveBeenCalledWith(
|
||||
"testToolbar.state",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("stops watching toolbar state when destroyed", function () {
|
||||
expect(mockUnwatch).not.toHaveBeenCalled();
|
||||
representer.destroy();
|
||||
expect(mockUnwatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// 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.push(testObject);
|
||||
expect(mockScope.$watchCollection.mostRecentCall.args[0])
|
||||
.toEqual('selection'); // Make sure we're using right watch
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]([testObject]);
|
||||
|
||||
// Update the state
|
||||
mockScope.$parent.testToolbar.state[0] = 456;
|
||||
mockScope.$parent.$watchCollection.mostRecentCall.args[1](
|
||||
mockScope.$parent.testToolbar.state
|
||||
);
|
||||
|
||||
// Should have updated the original object
|
||||
expect(testObject.k).toEqual(456);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
189
platform/commonUI/edit/test/representers/EditToolbarSpec.js
Normal file
189
platform/commonUI/edit/test/representers/EditToolbarSpec.js
Normal file
@ -0,0 +1,189 @@
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
['../../src/representers/EditToolbar'],
|
||||
function (EditToolbar) {
|
||||
"use strict";
|
||||
|
||||
describe("An Edit mode toolbar", function () {
|
||||
var testStructure,
|
||||
testAB,
|
||||
testABC,
|
||||
testABC2,
|
||||
testABCXYZ,
|
||||
testABCYZ;
|
||||
|
||||
beforeEach(function () {
|
||||
testStructure = {
|
||||
sections: [
|
||||
{
|
||||
items: [
|
||||
{ name: "A", property: "a" },
|
||||
{ name: "B", property: "b" },
|
||||
{ name: "C", property: "c" }
|
||||
]
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{ name: "X", property: "x", inclusive: true },
|
||||
{ name: "Y", property: "y" },
|
||||
{ name: "Z", property: "z" }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
testAB = { a: 0, b: 1 };
|
||||
testABC = { a: 0, b: 1, c: 2 };
|
||||
testABC2 = { a: 4, b: 1, c: 2 }; // For inconsistent-state checking
|
||||
testABCXYZ = { a: 0, b: 1, c: 2, x: 'X!', y: 'Y!', z: 'Z!' };
|
||||
testABCYZ = { a: 0, b: 1, c: 2, y: 'Y!', z: 'Z!' };
|
||||
});
|
||||
|
||||
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("prunes empty sections", function () {
|
||||
// Verify that all sections are included when applicable...
|
||||
expect(
|
||||
new EditToolbar(testStructure, [ testABCXYZ ])
|
||||
.getStructure()
|
||||
.sections
|
||||
.length
|
||||
).toEqual(2);
|
||||
// ...but omitted when only some are applicable
|
||||
expect(
|
||||
new EditToolbar(testStructure, [ testABC ])
|
||||
.getStructure()
|
||||
.sections
|
||||
.length
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("reads properties from selections", function () {
|
||||
var toolbar = new EditToolbar(testStructure, [ 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 toolbar, structure, state;
|
||||
|
||||
testABC.a = function () { return "from a getter!"; };
|
||||
|
||||
toolbar = new EditToolbar(testStructure, [ 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 () {
|
||||
var toolbar = new EditToolbar(testStructure, [ testABC ]),
|
||||
structure = toolbar.getStructure();
|
||||
toolbar.updateState(
|
||||
structure.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 toolbar, structure, state;
|
||||
|
||||
testABC.a = jasmine.createSpy('a');
|
||||
|
||||
toolbar = new EditToolbar(testStructure, [ 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("removes inapplicable items", function () {
|
||||
// First, verify with all items
|
||||
expect(
|
||||
new EditToolbar(testStructure, [ testABC ])
|
||||
.getStructure()
|
||||
.sections[0]
|
||||
.items
|
||||
.length
|
||||
).toEqual(3);
|
||||
// Then, try with some items omitted
|
||||
expect(
|
||||
new EditToolbar(testStructure, [ testABC, testAB ])
|
||||
.getStructure()
|
||||
.sections[0]
|
||||
.items
|
||||
.length
|
||||
).toEqual(2);
|
||||
});
|
||||
|
||||
it("removes inconsistent states", function () {
|
||||
// Only two of three values match among these selections
|
||||
expect(
|
||||
new EditToolbar(testStructure, [ testABC, testABC2 ])
|
||||
.getStructure()
|
||||
.sections[0]
|
||||
.items
|
||||
.length
|
||||
).toEqual(2);
|
||||
});
|
||||
|
||||
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
|
||||
expect(
|
||||
new EditToolbar(testStructure, [ testABC, testABCXYZ ])
|
||||
.getStructure()
|
||||
.sections[1]
|
||||
.items
|
||||
.length
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("removes inclusive items when there are no matches", function () {
|
||||
expect(
|
||||
new EditToolbar(testStructure, [ testABCYZ ])
|
||||
.getStructure()
|
||||
.sections[1]
|
||||
.items
|
||||
.length
|
||||
).toEqual(2);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -15,5 +15,7 @@
|
||||
"capabilities/EditorCapability",
|
||||
"objects/EditableDomainObject",
|
||||
"objects/EditableDomainObjectCache",
|
||||
"objects/EditableModelCache"
|
||||
"objects/EditableModelCache",
|
||||
"representers/EditToolbar",
|
||||
"representers/EditToolbarRepresenter"
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user