Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nasa/openmct.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Henry <akhenry@gmail.com>2022-07-25 20:43:49 +0300
committerAndrew Henry <akhenry@gmail.com>2022-07-25 20:43:49 +0300
commit5e1d558cf79989d1a6aa279a0ef7d409b4cc05de (patch)
treeabd6ef1ad9252335cb5324c73e431955437c8f29
parent488cd82ae168a39c3d64a8224077972035b56a2d (diff)
Finished observersmutation-paths-mark-2
-rw-r--r--src/api/objects/MutableDomainObject.js105
-rw-r--r--src/api/objects/ObjectAPISpec.js184
-rw-r--r--src/plugins/notebook/components/Notebook.vue5
-rw-r--r--src/plugins/notebook/components/NotebookEntry.vue7
-rw-r--r--src/ui/components/tags/TagEditor.vue8
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');
}
}
};