Compare commits
1 Commits
infinite-v
...
mutation-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e1d558cf7 |
@@ -52,8 +52,8 @@ class MutableDomainObject {
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
},
|
||||
_observers: {
|
||||
value: [],
|
||||
_callbacksForPaths: {
|
||||
value: {},
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
},
|
||||
@@ -64,15 +64,31 @@ class MutableDomainObject {
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* BRAND new approach
|
||||
* - Register a listener on $_synchronize_model
|
||||
* - The $_synchronize_model event provides the path. Figure out whether the mutated path is equal to, or a parent of the observed path.
|
||||
* - If so, trigger callback with new value
|
||||
* - As an optimization, ONLY trigger if value has actually changed. Could be deferred until later?
|
||||
*/
|
||||
|
||||
$observe(path, callback) {
|
||||
let fullPath = qualifiedEventName(this, path);
|
||||
let eventOff =
|
||||
this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
|
||||
let callbacksForPath = this._callbacksForPaths[path];
|
||||
if (callbacksForPath === undefined) {
|
||||
callbacksForPath = [];
|
||||
this._callbacksForPaths[path] = callbacksForPath;
|
||||
}
|
||||
|
||||
this._globalEventEmitter.on(fullPath, callback);
|
||||
this._observers.push(eventOff);
|
||||
callbacksForPath.push(callback);
|
||||
|
||||
return function unlisten() {
|
||||
let index = callbacksForPath.indexOf(callback);
|
||||
callbacksForPath.splice(index, 1);
|
||||
if (callbacksForPath.length === 0) {
|
||||
delete this._callbacksForPaths[path];
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
return eventOff;
|
||||
}
|
||||
$set(path, value) {
|
||||
_.set(this, path, value);
|
||||
@@ -88,25 +104,14 @@ class MutableDomainObject {
|
||||
this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this);
|
||||
//Emit wildcard event, with path so that callback knows what changed
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value);
|
||||
|
||||
//Emit events specific to properties affected
|
||||
let parentPropertiesList = path.split('.');
|
||||
for (let index = parentPropertiesList.length; index > 0; index--) {
|
||||
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath));
|
||||
}
|
||||
|
||||
//TODO: Emit events for listeners of child properties when parent changes.
|
||||
// Do it at observer time - also register observers for parent attribute path.
|
||||
}
|
||||
|
||||
$refresh(model) {
|
||||
//TODO: Currently we are updating the entire object.
|
||||
// In the future we could update a specific property of the object using the 'path' parameter.
|
||||
const clone = JSON.parse(JSON.stringify(this));
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model);
|
||||
|
||||
//Emit wildcard event, with path so that callback knows what changed
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this);
|
||||
//Emit wildcard event
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, '*', this, clone);
|
||||
}
|
||||
|
||||
$on(event, callback) {
|
||||
@@ -114,23 +119,53 @@ class MutableDomainObject {
|
||||
|
||||
return () => this._instanceEventEmitter.off(event, callback);
|
||||
}
|
||||
$destroy() {
|
||||
while (this._observers.length > 0) {
|
||||
const observer = this._observers.pop();
|
||||
observer();
|
||||
}
|
||||
$updateListenersOnPath(updatedModel, mutatedPath, newValue, oldModel) {
|
||||
const isRefresh = mutatedPath === '*';
|
||||
|
||||
Object.entries(this._callbacksForPaths).forEach(([observedPath, callbacks]) => {
|
||||
if (isChildOf(observedPath, mutatedPath)
|
||||
|| isParentOf(observedPath, mutatedPath)) {
|
||||
let newValueOfObservedPath;
|
||||
|
||||
if (observedPath === '*') {
|
||||
newValueOfObservedPath = updatedModel;
|
||||
|
||||
} else {
|
||||
newValueOfObservedPath = _.get(updatedModel, observedPath);
|
||||
}
|
||||
|
||||
if (isRefresh && observedPath !== '*') {
|
||||
const oldValueOfObservedPath = _.get(oldModel, observedPath);
|
||||
if (!_.isEqual(newValueOfObservedPath, oldValueOfObservedPath)) {
|
||||
callbacks.forEach(callback => callback(newValueOfObservedPath));
|
||||
}
|
||||
} else {
|
||||
//Assumed to be different if result of mutation.
|
||||
callbacks.forEach(callback => callback(newValueOfObservedPath));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
$synchronizeModel(updatedObject) {
|
||||
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||
utils.refresh(this, clone);
|
||||
}
|
||||
$destroy() {
|
||||
Object.keys(this._callbacksForPaths).forEach(key => delete this._callbacksForPaths[key]);
|
||||
this._instanceEventEmitter.emit('$_destroy');
|
||||
this._globalEventEmitter.off(qualifiedEventName(this, '$_synchronize_model'), this.$synchronizeModel);
|
||||
this._globalEventEmitter.off(qualifiedEventName(this, '*'), this.$updateListenersOnPath);
|
||||
}
|
||||
|
||||
static createMutable(object, mutationTopic) {
|
||||
let mutable = Object.create(new MutableDomainObject(mutationTopic));
|
||||
Object.assign(mutable, object);
|
||||
|
||||
mutable.$observe('$_synchronize_model', (updatedObject) => {
|
||||
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||
utils.refresh(mutable, clone);
|
||||
});
|
||||
mutable.$updateListenersOnPath = mutable.$updateListenersOnPath.bind(mutable);
|
||||
mutable.$synchronizeModel = mutable.$synchronizeModel.bind(mutable);
|
||||
|
||||
mutable._globalEventEmitter.on(qualifiedEventName(mutable, '$_synchronize_model'), mutable.$synchronizeModel);
|
||||
mutable._globalEventEmitter.on(qualifiedEventName(mutable, '*'), mutable.$updateListenersOnPath);
|
||||
|
||||
return mutable;
|
||||
}
|
||||
@@ -147,4 +182,12 @@ function qualifiedEventName(object, eventName) {
|
||||
return [keystring, eventName].join(':');
|
||||
}
|
||||
|
||||
function isChildOf(observedPath, mutatedPath) {
|
||||
return Boolean(mutatedPath === '*' || observedPath?.startsWith(mutatedPath));
|
||||
}
|
||||
|
||||
function isParentOf(observedPath, mutatedPath) {
|
||||
return Boolean(observedPath === '*' || mutatedPath?.startsWith(observedPath));
|
||||
}
|
||||
|
||||
export default MutableDomainObject;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ObjectAPI from './ObjectAPI.js';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
describe("The Object API", () => {
|
||||
fdescribe("The Object API", () => {
|
||||
let objectAPI;
|
||||
let typeRegistry;
|
||||
let openmct = {};
|
||||
@@ -287,53 +287,167 @@ describe("The Object API", () => {
|
||||
mutableSecondInstance.$destroy();
|
||||
});
|
||||
|
||||
it('to stay synchronized when mutated', function () {
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
describe('on mutation', () => {
|
||||
it('to stay synchronized', function () {
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
});
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
});
|
||||
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
}
|
||||
});
|
||||
});
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
}
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a parent property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined);
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: 'updated-embedded-value'
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('on refresh', () => {
|
||||
let refreshModel;
|
||||
|
||||
beforeEach(() => {
|
||||
refreshModel = JSON.parse(JSON.stringify(mutable));
|
||||
});
|
||||
|
||||
it('to stay synchronized', function () {
|
||||
refreshModel.otherAttribute = 'new-attribute-value';
|
||||
mutable.$refresh(refreshModel);
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
refreshModel.otherAttribute = 'some-new-value';
|
||||
mutable.$refresh(refreshModel);
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
refreshModel.objectAttribute.embeddedObject.embeddedKey = 'updated-embedded-value';
|
||||
mutable.$refresh(refreshModel);
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
});
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
}
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a parent property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
refreshModel.objectAttribute.embeddedObject = 'updated-embedded-value';
|
||||
|
||||
mutable.$refresh(refreshModel);
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined);
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: 'updated-embedded-value'
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -296,12 +296,17 @@ export default {
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.filterAndSortEntries();
|
||||
this.unlistenToEntryChanges = this.openmct.objects.observe(this.domainObject, "configuration.entries", () => this.filterAndSortEntries());
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
if (this.unlistenToEntryChanges) {
|
||||
this.unlistenToEntryChanges();
|
||||
}
|
||||
|
||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
},
|
||||
|
||||
@@ -233,6 +233,13 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.dropOnEntry = this.dropOnEntry.bind(this);
|
||||
this.$on('tags-updated', async () => {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
this.entry.modified = Date.now();
|
||||
this.entry.modifiedBy = user.getId();
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async addNewEmbed(objectPath) {
|
||||
|
||||
@@ -133,8 +133,11 @@ export default {
|
||||
this.addedTags.push(newTagValue);
|
||||
this.userAddingTag = true;
|
||||
},
|
||||
tagRemoved(tagToRemove) {
|
||||
return this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||
async tagRemoved(tagToRemove) {
|
||||
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||
this.$emit('tags-updated');
|
||||
|
||||
return result;
|
||||
},
|
||||
async tagAdded(newTag) {
|
||||
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
|
||||
@@ -146,6 +149,7 @@ export default {
|
||||
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
this.userAddingTag = false;
|
||||
this.$emit('tags-updated');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user