Compare commits
	
		
			1 Commits
		
	
	
		
			fix-remote
			...
			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