Remove duplicate policy (#2399)

* Removed policy preventing duplicate composition, and implemented no-op in composition provider instead

* Change order of edit on drop event listener

* Add mutation listener to CompositionCollection even if nothing listening to collection

* Updated test specs

* Address review comments
This commit is contained in:
Andrew Henry 2019-05-20 19:14:12 -07:00 committed by Pegah Sarram
parent b5e23963d4
commit 526b4aa07e
4 changed files with 99 additions and 71 deletions

View File

@ -22,8 +22,20 @@ define([
publicAPI = {}; publicAPI = {};
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [ publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
'get', 'get',
'mutate' 'mutate',
'observe',
'areIdsEqual'
]); ]);
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
return id1.namespace === id2.namespace && id1.key === id2.key;
});
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
'checkPolicy'
]);
publicAPI.composition.checkPolicy.and.returnValue(true);
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [ publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
'on' 'on'
]); ]);
@ -91,7 +103,7 @@ define([
beforeEach(function () { beforeEach(function () {
listener = jasmine.createSpy('reorderListener'); listener = jasmine.createSpy('reorderListener');
composition.on('reorder', listener); composition.on('reorder', listener);
return composition.load(); return composition.load();
}); });
it('', function () { it('', function () {
@ -119,49 +131,16 @@ define([
expect(newComposition[2].key).toEqual('a'); expect(newComposition[2].key).toEqual('a');
}) })
}); });
it('supports adding an object to composition', function () {
// TODO: Implement add/removal in new default provider. let addListener = jasmine.createSpy('addListener');
xit('synchronizes changes between instances', function () { let mockChildObject = {
var otherComposition = compositionAPI.get(domainObject); identifier: {key: 'mock-key', namespace: ''}
var addListener = jasmine.createSpy('addListener'); };
var removeListener = jasmine.createSpy('removeListener');
var otherAddListener = jasmine.createSpy('otherAddListener');
var otherRemoveListener = jasmine.createSpy('otherRemoveListener');
composition.on('add', addListener); composition.on('add', addListener);
composition.on('remove', removeListener); composition.add(mockChildObject);
otherComposition.on('add', otherAddListener);
otherComposition.on('remove', otherRemoveListener);
return Promise.all([composition.load(), otherComposition.load()]) expect(domainObject.composition.length).toBe(4);
.then(function () { expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
expect(addListener).toHaveBeenCalled();
expect(otherAddListener).toHaveBeenCalled();
expect(removeListener).not.toHaveBeenCalled();
expect(otherRemoveListener).not.toHaveBeenCalled();
var object = addListener.calls.mostRecent().args[0];
composition.remove(object);
expect(removeListener).toHaveBeenCalled();
expect(otherRemoveListener).toHaveBeenCalled();
addListener.reset();
otherAddListener.reset();
composition.add(object);
expect(addListener).toHaveBeenCalled();
expect(otherAddListener).toHaveBeenCalled();
removeListener.reset();
otherRemoveListener.reset();
otherComposition.remove(object);
expect(removeListener).toHaveBeenCalled();
expect(otherRemoveListener).toHaveBeenCalled();
addListener.reset();
otherAddListener.reset();
otherComposition.add(object);
expect(addListener).toHaveBeenCalled();
expect(otherAddListener).toHaveBeenCalled();
});
}); });
}); });
@ -184,7 +163,9 @@ define([
key: 'thing' key: 'thing'
} }
]); ]);
} },
add: jasmine.createSpy('add'),
remove: jasmine.createSpy('remove')
}; };
domainObject = { domainObject = {
identifier: { identifier: {
@ -214,6 +195,25 @@ define([
}); });
}); });
}); });
describe('Calling add or remove', function () {
let mockChildObject;
beforeEach(function () {
mockChildObject = {
identifier: {key: 'mock-key', namespace: ''}
};
composition.add(mockChildObject);
});
it('calls add on the provider', function () {
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
it('calls remove on the provider', function () {
composition.remove(mockChildObject);
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
});
}); });
describe('dynamic custom composition', function () { describe('dynamic custom composition', function () {

View File

@ -75,9 +75,7 @@ define([
throw new Error('Event not supported by composition: ' + event); throw new Error('Event not supported by composition: ' + event);
} }
if (!this.mutationListener) { if (!this.mutationListener) {
this.mutationListener = this.publicAPI.objects.observe(this.domainObject, '*', (newDomainObject) => { this._synchronize();
this.domainObject = newDomainObject;
})
} }
if (this.provider.on && this.provider.off) { if (this.provider.on && this.provider.off) {
if (event === 'add') { if (event === 'add') {
@ -134,10 +132,8 @@ define([
this.listeners[event].splice(index, 1); this.listeners[event].splice(index, 1);
if (this.listeners[event].length === 0) { if (this.listeners[event].length === 0) {
if (this.mutationListener) { this._destroy();
this.mutationListener();
delete this.mutationListener;
}
// Remove provider listener if this is the last callback to // Remove provider listener if this is the last callback to
// be removed. // be removed.
if (this.provider.off && this.provider.on) { if (this.provider.off && this.provider.on) {
@ -181,6 +177,9 @@ define([
*/ */
CompositionCollection.prototype.add = function (child, skipMutate) { CompositionCollection.prototype.add = function (child, skipMutate) {
if (!skipMutate) { if (!skipMutate) {
if (!this.publicAPI.composition.checkPolicy(this.domainObject, child)) {
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
}
this.provider.add(this.domainObject, child.identifier); this.provider.add(this.domainObject, child.identifier);
} else { } else {
this.emit('add', child); this.emit('add', child);
@ -272,6 +271,19 @@ define([
this.remove(child, true); this.remove(child, true);
}; };
CompositionCollection.prototype._synchronize = function () {
this.mutationListener = this.publicAPI.objects.observe(this.domainObject, '*', (newDomainObject) => {
this.domainObject = JSON.parse(JSON.stringify(newDomainObject));
});
};
CompositionCollection.prototype._destroy = function () {
if (this.mutationListener) {
this.mutationListener();
delete this.mutationListener;
}
};
/** /**
* Emit events. * Emit events.
* @private * @private

View File

@ -48,24 +48,11 @@ define([
this.listeningTo = {}; this.listeningTo = {};
this.onMutation = this.onMutation.bind(this); this.onMutation = this.onMutation.bind(this);
this.cannotContainDuplicates = this.cannotContainDuplicates.bind(this);
this.cannotContainItself = this.cannotContainItself.bind(this); this.cannotContainItself = this.cannotContainItself.bind(this);
compositionAPI.addPolicy(this.cannotContainDuplicates);
compositionAPI.addPolicy(this.cannotContainItself); compositionAPI.addPolicy(this.cannotContainItself);
} }
/**
* @private
*/
DefaultCompositionProvider.prototype.cannotContainDuplicates = function (parent, child) {
return this.appliesTo(parent) &&
parent.composition.findIndex((composeeId) => {
return composeeId.namespace === child.identifier.namespace &&
composeeId.key === child.identifier.key;
}) === -1;
}
/** /**
* @private * @private
*/ */
@ -199,9 +186,18 @@ define([
* @memberof module:openmct.CompositionProvider# * @memberof module:openmct.CompositionProvider#
* @method add * @method add
*/ */
DefaultCompositionProvider.prototype.add = function (domainObject, child) { DefaultCompositionProvider.prototype.add = function (parent, childId) {
throw new Error('Default Provider does not implement adding.'); if (!this.includes(parent, childId)) {
// TODO: this needs to be synchronized via mutation parent.composition.push(childId);
this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
}
};
/**
* @private
*/
DefaultCompositionProvider.prototype.includes = function (parent, childId) {
return parent.composition.findIndex(composee =>
this.publicAPI.objects.areIdsEqual(composee, childId)) !== -1;
}; };
DefaultCompositionProvider.prototype.reorder = function (domainObject, oldIndex, newIndex) { DefaultCompositionProvider.prototype.reorder = function (domainObject, oldIndex, newIndex) {

View File

@ -34,10 +34,10 @@ export default {
this.currentObject = this.object; this.currentObject = this.object;
this.updateView(); this.updateView();
this.$el.addEventListener('dragover', this.onDragOver); this.$el.addEventListener('dragover', this.onDragOver);
this.$el.addEventListener('drop', this.addObjectToParent);
this.$el.addEventListener('drop', this.editIfEditable, { this.$el.addEventListener('drop', this.editIfEditable, {
capture: true capture: true
}); });
this.$el.addEventListener('drop', this.addObjectToParent);
}, },
methods: { methods: {
clear() { clear() {
@ -57,6 +57,10 @@ export default {
this.removeSelectable(); this.removeSelectable();
delete this.removeSelectable; delete this.removeSelectable;
} }
if (this.composition) {
this.composition._destroy();
}
}, },
invokeEditModeHandler(editMode) { invokeEditModeHandler(editMode) {
this.currentView.onEditModeChange(editMode); this.currentView.onEditModeChange(editMode);
@ -112,14 +116,27 @@ export default {
delete this.removeSelectable; delete this.removeSelectable;
} }
if (this.composition) {
this.composition._destroy();
}
this.currentObject = object; this.currentObject = object;
this.unlisten = this.openmct.objects.observe(this.currentObject, '*', (mutatedObject) => { this.unlisten = this.openmct.objects.observe(this.currentObject, '*', (mutatedObject) => {
this.currentObject = mutatedObject; this.currentObject = mutatedObject;
}); });
this.composition = this.openmct.composition.get(this.currentObject);
if (this.composition) {
this.composition._synchronize();
this.loadComposition();
}
this.viewKey = viewKey; this.viewKey = viewKey;
this.updateView(immediatelySelect); this.updateView(immediatelySelect);
}, },
loadComposition() {
return this.composition.load();
},
getSelectionContext() { getSelectionContext() {
if (this.currentView.getSelectionContext) { if (this.currentView.getSelectionContext) {
return this.currentView.getSelectionContext(); return this.currentView.getSelectionContext();
@ -133,10 +150,12 @@ export default {
} }
}, },
addObjectToParent(event) { addObjectToParent(event) {
if (this.hasComposableDomainObject(event)) { if (this.hasComposableDomainObject(event) && this.composition) {
let composableDomainObject = this.getComposableDomainObject(event); let composableDomainObject = this.getComposableDomainObject(event);
this.currentObject.composition.push(composableDomainObject.identifier); this.loadComposition().then(() => {
this.openmct.objects.mutate(this.currentObject, 'composition', this.currentObject.composition); this.composition.add(composableDomainObject);
});
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
@ -155,6 +174,7 @@ export default {
editIfEditable(event) { editIfEditable(event) {
let provider = this.getViewProvider(); let provider = this.getViewProvider();
if (provider && if (provider &&
provider.canEdit &&
provider.canEdit(this.currentObject) && provider.canEdit(this.currentObject) &&
!this.openmct.editor.isEditing()) { !this.openmct.editor.isEditing()) {
this.openmct.editor.edit(); this.openmct.editor.edit();