diff options
author | Andrew Henry <akhenry@gmail.com> | 2022-07-25 20:43:49 +0300 |
---|---|---|
committer | Andrew Henry <akhenry@gmail.com> | 2022-07-25 20:43:49 +0300 |
commit | 5e1d558cf79989d1a6aa279a0ef7d409b4cc05de (patch) | |
tree | abd6ef1ad9252335cb5324c73e431955437c8f29 | |
parent | 488cd82ae168a39c3d64a8224077972035b56a2d (diff) |
Finished observersmutation-paths-mark-2
-rw-r--r-- | src/api/objects/MutableDomainObject.js | 105 | ||||
-rw-r--r-- | src/api/objects/ObjectAPISpec.js | 184 | ||||
-rw-r--r-- | src/plugins/notebook/components/Notebook.vue | 5 | ||||
-rw-r--r-- | src/plugins/notebook/components/NotebookEntry.vue | 7 | ||||
-rw-r--r-- | src/ui/components/tags/TagEditor.vue | 8 |
5 files changed, 241 insertions, 68 deletions
diff --git a/src/api/objects/MutableDomainObject.js b/src/api/objects/MutableDomainObject.js index 046f8b069..94f8b226d 100644 --- a/src/api/objects/MutableDomainObject.js +++ b/src/api/objects/MutableDomainObject.js @@ -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); } + $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() { - while (this._observers.length > 0) { - const observer = this._observers.pop(); - observer(); - } - + 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; diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js index 8887a04c7..a735c1400 100644 --- a/src/api/objects/ObjectAPISpec.js +++ b/src/api/objects/ObjectAPISpec.js @@ -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'); - }); + 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 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(); + }); + }); + + 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()); + }); + }); + + 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' + }); - 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(); + listeners.forEach(listener => listener()); + }); }); }); - 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 = []; + describe('on refresh', () => { + let refreshModel; - return new Promise(function (resolve) { - objectAttributeCallback.and.callFake(resolve); + beforeEach(() => { + refreshModel = JSON.parse(JSON.stringify(mutable)); + }); - listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback)); - listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback)); - listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)); + it('to stay synchronized', function () { + refreshModel.otherAttribute = 'new-attribute-value'; + mutable.$refresh(refreshModel); + expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value'); + }); - objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value'); - }).then(function () { - expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value'); - expect(embeddedObjectCallback).toHaveBeenCalledWith({ - embeddedKey: 'updated-embedded-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(); }); - 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)); + + 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)); - listeners.forEach(listener => listener()); + 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()); + }); }); }); + }); }); diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index 4d493525a..01a83ab5b 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -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); }, diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index f90aecb03..36f02eb15 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -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) { diff --git a/src/ui/components/tags/TagEditor.vue b/src/ui/components/tags/TagEditor.vue index 4f581d996..90b3d79a1 100644 --- a/src/ui/components/tags/TagEditor.vue +++ b/src/ui/components/tags/TagEditor.vue @@ -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'); } } }; |