From 6116351dad935c280895d7ee2af211fa0b7accbf Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 19 Mar 2019 10:31:56 -0700 Subject: [PATCH] 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 --- src/api/composition/CompositionAPISpec.js | 43 ++++++++- src/api/composition/CompositionCollection.js | 50 +++++++++- .../composition/DefaultCompositionProvider.js | 27 +++++- .../LADTable/LADTableSetViewProvider.js | 3 + src/plugins/LADTable/LADTableViewProvider.js | 3 + src/plugins/LADTable/components/LADTable.vue | 7 ++ .../LADTable/components/LadTableSet.vue | 8 ++ src/ui/inspector/Elements.vue | 96 ++++++++++--------- src/ui/router/Browse.js | 10 ++ 9 files changed, 194 insertions(+), 53 deletions(-) diff --git a/src/api/composition/CompositionAPISpec.js b/src/api/composition/CompositionAPISpec.js index 95258d81b5..5dc2d76350 100644 --- a/src/api/composition/CompositionAPISpec.js +++ b/src/api/composition/CompositionAPISpec.js @@ -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 () { diff --git a/src/api/composition/CompositionCollection.js b/src/api/composition/CompositionCollection.js index 3834e84c3c..48f9b1a50f 100644 --- a/src/api/composition/CompositionCollection.js +++ b/src/api/composition/CompositionCollection.js @@ -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); } }); }; diff --git a/src/api/composition/DefaultCompositionProvider.js b/src/api/composition/DefaultCompositionProvider.js index aa58e2cc20..f8bf88dd7b 100644 --- a/src/api/composition/DefaultCompositionProvider.js +++ b/src/api/composition/DefaultCompositionProvider.js @@ -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. diff --git a/src/plugins/LADTable/LADTableSetViewProvider.js b/src/plugins/LADTable/LADTableSetViewProvider.js index 6f79cd457d..f11caa64b6 100644 --- a/src/plugins/LADTable/LADTableSetViewProvider.js +++ b/src/plugins/LADTable/LADTableSetViewProvider.js @@ -35,6 +35,9 @@ define([ canView: function (domainObject) { return domainObject.type === 'LadTableSet'; }, + canEdit: function (domainObject) { + return domainObject.type === 'LadTableSet'; + }, view: function (domainObject) { let component; diff --git a/src/plugins/LADTable/LADTableViewProvider.js b/src/plugins/LADTable/LADTableViewProvider.js index e2b151e98f..8e86c1b617 100644 --- a/src/plugins/LADTable/LADTableViewProvider.js +++ b/src/plugins/LADTable/LADTableViewProvider.js @@ -35,6 +35,9 @@ define([ canView: function (domainObject) { return domainObject.type === 'LadTable'; }, + canEdit: function (domainObject) { + return domainObject.type === 'LadTable'; + }, view: function (domainObject) { let component; diff --git a/src/plugins/LADTable/components/LADTable.vue b/src/plugins/LADTable/components/LADTable.vue index 339c7d45b0..0b684dcb69 100644 --- a/src/plugins/LADTable/components/LADTable.vue +++ b/src/plugins/LADTable/components/LADTable.vue @@ -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); } } diff --git a/src/plugins/LADTable/components/LadTableSet.vue b/src/plugins/LADTable/components/LadTableSet.vue index 577700906e..23c39b76c5 100644 --- a/src/plugins/LADTable/components/LadTableSet.vue +++ b/src/plugins/LADTable/components/LadTableSet.vue @@ -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); diff --git a/src/ui/inspector/Elements.vue b/src/ui/inspector/Elements.vue index f5ad0bf473..7acea28c27 100644 --- a/src/ui/inspector/Elements.vue +++ b/src/ui/inspector/Elements.vue @@ -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(); + } } } diff --git a/src/ui/router/Browse.js b/src/ui/router/Browse.js index cd88c8e5a7..61a71669e4 100644 --- a/src/ui/router/Browse.js +++ b/src/ui/router/Browse.js @@ -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('/'); } @@ -36,6 +42,10 @@ define([ // navigation service and router to expose a clear and minimal // 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;