Reorder api (#2316)

* Added 'reorder' function to composition API

* Re-implemented reordering in Elements

* Make LAD table editable

* Remove test spec focus

* Fixing bugs with event listeners

* Clean up listeners properly in Elements pool

* Fixed race condition on drag-and-drop to initiate edit

* Implement reordering in LAD tables
This commit is contained in:
Andrew Henry 2019-03-19 10:31:56 -07:00 committed by Deep Tailor
parent 23efef4469
commit 6116351dad
9 changed files with 194 additions and 53 deletions

View File

@ -21,7 +21,11 @@ define([
topicService.and.returnValue(mutationTopic);
publicAPI = {};
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
'get'
'get',
'mutate'
]);
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
'on'
]);
publicAPI.objects.get.and.callFake(function (identifier) {
return Promise.resolve({identifier: identifier});
@ -52,6 +56,14 @@ define([
{
namespace: 'test',
key: 'a'
},
{
namespace: 'test',
key: 'b'
},
{
namespace: 'test',
key: 'c'
}
]
};
@ -68,12 +80,39 @@ define([
composition.on('add', listener);
return composition.load().then(function () {
expect(listener.calls.count()).toBe(1);
expect(listener.calls.count()).toBe(3);
expect(listener).toHaveBeenCalledWith({
identifier: {namespace: 'test', key: 'a'}
});
});
});
describe('supports reordering of composition', function () {
var listener;
beforeEach(function () {
listener = jasmine.createSpy('reorderListener');
composition.on('reorder', listener);
return composition.load();
});
it('', function () {
composition.reorder(1, 0);
let newComposition =
publicAPI.objects.mutate.calls.mostRecent().args[2];
expect(listener).toHaveBeenCalledWith(1, 0);
expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('a');
});
it('', function () {
composition.reorder(0, 2);
let newComposition =
publicAPI.objects.mutate.calls.mostRecent().args[2];
expect(listener).toHaveBeenCalledWith(0, 2);
expect(newComposition[0].key).toEqual('c');
expect(newComposition[2].key).toEqual('a');
})
});
// TODO: Implement add/removal in new default provider.
xit('synchronizes changes between instances', function () {

View File

@ -56,7 +56,8 @@ define([
this.listeners = {
add: [],
remove: [],
load: []
load: [],
reorder: []
};
this.onProviderAdd = this.onProviderAdd.bind(this);
this.onProviderRemove = this.onProviderRemove.bind(this);
@ -91,6 +92,13 @@ define([
this.onProviderRemove,
this
);
} if (event === 'reorder') {
this.provider.on(
this.domainObject,
'reorder',
this.onProviderReorder,
this
)
}
}
@ -141,6 +149,13 @@ define([
this.onProviderRemove,
this
);
} else if (event === 'reorder') {
this.provider.off(
this.domainObject,
'reorder',
this.onProviderReorder,
this
);
}
}
}
@ -209,6 +224,33 @@ define([
}
};
/**
* Reorder the domain objects in this composition.
*
* A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method.
*
* @param {number} oldIndex
* @param {number} newIndex
* @memberof module:openmct.CompositionCollection#
* @name remove
*/
CompositionCollection.prototype.reorder = function (oldIndex, newIndex, skipMutate) {
if (!skipMutate) {
this.provider.reorder(this.domainObject, oldIndex, newIndex);
} else {
this.emit('reorder', oldIndex, newIndex);
}
};
/**
* Handle reorder from provider.
* @private
*/
CompositionCollection.prototype.onProviderReorder = function (oldIndex, newIndex) {
this.reorder(oldIndex, newIndex, true);
};
/**
* Handle adds from provider.
* @private
@ -232,12 +274,12 @@ define([
* Emit events.
* @private
*/
CompositionCollection.prototype.emit = function (event, payload) {
CompositionCollection.prototype.emit = function (event, ...payload) {
this.listeners[event].forEach(function (l) {
if (l.context) {
l.callback.call(l.context, payload);
l.callback.apply(l.context, payload);
} else {
l.callback(payload);
l.callback(...payload);
}
});
};

View File

@ -126,6 +126,7 @@ define([
objectListeners = this.listeningTo[keyString] = {
add: [],
remove: [],
reorder: [],
composition: [].slice.apply(domainObject.composition)
};
}
@ -160,7 +161,7 @@ define([
});
objectListeners[event].splice(index, 1);
if (!objectListeners.add.length && !objectListeners.remove.length) {
if (!objectListeners.add.length && !objectListeners.remove.length && !objectListeners.reorder.length) {
delete this.listeningTo[keyString];
}
};
@ -203,6 +204,30 @@ define([
// TODO: this needs to be synchronized via mutation
};
DefaultCompositionProvider.prototype.reorder = function (domainObject, oldIndex, newIndex) {
let newComposition = domainObject.composition.slice();
newComposition[newIndex] = domainObject.composition[oldIndex];
newComposition[oldIndex] = domainObject.composition[newIndex];
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
let id = objectUtils.makeKeyString(domainObject.identifier);
var listeners = this.listeningTo[id];
if (!listeners) {
return;
}
listeners.reorder.forEach(notify);
function notify(listener) {
if (listener.context) {
listener.callback.call(listener.context, oldIndex, newIndex);
} else {
listener.callback(oldIndex, newIndex);
}
}
};
/**
* Listens on general mutation topic, using injector to fetch to avoid
* circular dependencies.

View File

@ -35,6 +35,9 @@ define([
canView: function (domainObject) {
return domainObject.type === 'LadTableSet';
},
canEdit: function (domainObject) {
return domainObject.type === 'LadTableSet';
},
view: function (domainObject) {
let component;

View File

@ -35,6 +35,9 @@ define([
canView: function (domainObject) {
return domainObject.type === 'LadTable';
},
canEdit: function (domainObject) {
return domainObject.type === 'LadTable';
},
view: function (domainObject) {
let component;

View File

@ -65,17 +65,24 @@ export default {
let index = _.findIndex(this.items, (item) => this.openmct.objects.makeKeyString(identifier) === item.key);
this.items.splice(index, 1);
},
reorder(oldIndex, newIndex) {
let objectAtOldIndex = this.items[oldIndex];
this.$set(this.items, oldIndex, this.items[newIndex]);
this.$set(this.items, newIndex, objectAtOldIndex);
}
},
mounted() {
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addItem);
this.composition.on('remove', this.removeItem);
this.composition.on('reorder', this.reorder);
this.composition.load();
},
destroyed() {
this.composition.off('add', this.addItem);
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.reorder);
}
}
</script>

View File

@ -93,6 +93,12 @@
this.primaryTelemetryObjects.splice(index,1);
primary = undefined;
},
reorderPrimary(oldIndex, newIndex) {
let objectAtOldIndex = this.primaryTelemetryObjects[oldIndex];
this.$set(this.primaryTelemetryObjects, oldIndex, this.primaryTelemetryObjects[newIndex]);
this.$set(this.primaryTelemetryObjects, newIndex, objectAtOldIndex);
},
addSecondary(primary) {
return (domainObject) => {
let secondary = {};
@ -120,11 +126,13 @@
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addPrimary);
this.composition.on('remove', this.removePrimary);
this.composition.on('reorder', this.reorderPrimary);
this.composition.load();
},
destroyed() {
this.composition.off('add', this.addPrimary);
this.composition.off('remove', this.removePrimary);
this.composition.off('reorder', this.reorderPrimary);
this.compositions.forEach(c => {
c.composition.off('add', c.addCallback);
c.composition.off('remove', c.removeCallback);

View File

@ -83,85 +83,89 @@ export default {
this.showSelection(selection);
}
this.openmct.selection.on('change', this.showSelection);
this.openmct.editor.on('isEditing', (isEditing)=>{
this.isEditing = isEditing;
this.showSelection(this.openmct.selection.get());
});
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
this.showSelection(this.openmct.selection.get());
},
showSelection(selection) {
this.elements = [];
this.elementsCache = [];
this.elementsCache = {};
this.listeners = [];
this.parentObject = selection[0].context.item;
if (this.mutationUnobserver) {
this.mutationUnobserver();
}
if (this.compositionUnlistener) {
this.compositionUnlistener();
}
if (this.parentObject) {
this.mutationUnobserver = this.openmct.objects.observe(this.parentObject, '*', (updatedModel) => {
this.parentObject = updatedModel;
this.refreshComposition();
});
this.refreshComposition();
this.composition = this.openmct.composition.get(this.parentObject);
if (this.composition) {
this.composition.load();
this.composition.on('add', this.addElement);
this.composition.on('remove', this.removeElement);
this.composition.on('reorder', this.reorderElements);
this.compositionUnlistener = () => {
this.composition.off('add', this.addElement);
this.composition.off('remove', this.removeElement);
this.composition.off('reorder', this.reorderElements);
delete this.compositionUnlistener;
}
}
}
},
refreshComposition() {
let composition = this.openmct.composition.get(this.parentObject);
if (composition){
composition.load().then(this.setElements);
}
addElement(element) {
let keyString = this.openmct.objects.makeKeyString(element.identifier);
this.elementsCache[keyString] =
JSON.parse(JSON.stringify(element));
this.applySearch(this.currentSearch);
},
setElements(elements) {
this.elementsCache = elements.map((element)=>JSON.parse(JSON.stringify(element)))
reorderElements() {
this.applySearch(this.currentSearch);
},
removeElement(identifier) {
let keyString = this.openmct.objects.makeKeyString(element.identifier);
delete this.elementsCache[keyString];
this.applySearch(this.currentSearch);
},
applySearch(input) {
this.currentSearch = input;
this.elements = this.elementsCache.filter((element) => {
return element.name.toLowerCase().search(
this.currentSearch) !== -1;
this.elements = this.parentObject.composition.map((id) =>
this.elementsCache[this.openmct.objects.makeKeyString(id)]
).filter((element) => {
return element !== undefined &&
element.name.toLowerCase().search(this.currentSearch) !== -1;
});
},
addObject(child){
this.elementsCache.push(child);
this.applySearch(this.currentSearch);
},
removeObject(childId){
this.elementsCache = this.elementsCache.filter((element) => !matches(element, childId));
this.applySearch(this.currentSearch);
function matches(elementA, elementBId) {
return elementA.identifier.namespace === elementBId.namespace &&
elementA.identifier.key === elementBId.key;
}
},
allowDrop(event) {
event.preventDefault();
},
moveTo(moveToIndex) {
console.log('dropped');
let composition = this.parentObject.composition;
let moveFromId = composition[this.moveFromIndex];
let deleteIndex = this.moveFromIndex;
if (moveToIndex < this.moveFromIndex) {
composition.splice(deleteIndex, 1);
composition.splice(moveToIndex, 0, moveFromId);
} else {
composition.splice(deleteIndex, 1);
composition.splice(moveToIndex, 0, moveFromId);
}
this.openmct.objects.mutate(this.parentObject, 'composition', composition);
this.composition.reorder(this.moveFromIndex, moveToIndex);
},
moveFrom(index){
this.moveFromIndex = index;
}
},
destroyed() {
this.openmct.editor.off('isEditing', this.setEditState);
this.openmct.selection.off('change', this.showSelection);
if (this.mutationUnobserver) {
this.mutationUnobserver();
}
if (this.compositionUnlistener) {
this.compositionUnlistener();
}
}
}
</script>

View File

@ -7,6 +7,7 @@ define([
return function install(openmct) {
let navigateCall = 0;
let browseObject;
let unobserve = undefined;
function viewObject(object, viewProvider) {
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true);
@ -18,6 +19,11 @@ define([
navigateCall++;
let currentNavigation = navigateCall;
if (unobserve) {
unobserve();
unobserve = undefined;
}
if (!Array.isArray(path)) {
path = path.split('/');
}
@ -37,6 +43,10 @@ define([
// API for this.
openmct.router.path = objects.reverse();
unobserve = this.openmct.objects.observe(openmct.router.path[0], '*', (newObject) => {
openmct.router.path[0] = newObject;
});
openmct.layout.$refs.browseBar.domainObject = navigatedObject;
browseObject = navigatedObject;
if (!navigatedObject) {