mirror of
https://github.com/nasa/openmct.git
synced 2025-05-19 17:03:30 +00:00
Fix multi user notebook (#5563)
* Detect remote changes to notebook object and re-render entries. Detect changes to tags as well * Do not throw an error when getCurrentUser is called, just return undefined. Code needs a way of testing whether there is a valid user * Support remote sync of annotations for notebook entries * Fix bug in notebook spec that prevented multi-user notebook regression being detected * Fixes edge case where an annotation does not initially exist * Use structuredClone instead of JSON functions. Fix logical error in entry modification attribution. Address magical value Co-authored-by: Scott Bell <scott@traclabs.com>
This commit is contained in:
parent
8dc8a1c0a9
commit
60f20c64d5
@ -88,7 +88,7 @@ export default class ObjectAPI {
|
|||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.interceptorRegistry = new InterceptorRegistry();
|
this.interceptorRegistry = new InterceptorRegistry();
|
||||||
|
|
||||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
|
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation'];
|
||||||
|
|
||||||
this.errors = {
|
this.errors = {
|
||||||
Conflict: ConflictError
|
Conflict: ConflictError
|
||||||
|
@ -83,9 +83,11 @@ class UserAPI extends EventEmitter {
|
|||||||
* @throws Will throw an error if no user provider is set
|
* @throws Will throw an error if no user provider is set
|
||||||
*/
|
*/
|
||||||
getCurrentUser() {
|
getCurrentUser() {
|
||||||
this.noProviderCheck();
|
if (!this.hasProvider()) {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
return this._provider.getCurrentUser();
|
} else {
|
||||||
|
return this._provider.getCurrentUser();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -296,12 +296,17 @@ export default {
|
|||||||
window.addEventListener('orientationchange', this.formatSidebar);
|
window.addEventListener('orientationchange', this.formatSidebar);
|
||||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||||
this.filterAndSortEntries();
|
this.filterAndSortEntries();
|
||||||
|
this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.unlisten) {
|
if (this.unlisten) {
|
||||||
this.unlisten();
|
this.unlisten();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.unobserveEntries) {
|
||||||
|
this.unobserveEntries();
|
||||||
|
}
|
||||||
|
|
||||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||||
},
|
},
|
||||||
|
@ -88,6 +88,7 @@
|
|||||||
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
||||||
:annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
|
:annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
|
||||||
:target-specific-details="{entryId: entry.id}"
|
:target-specific-details="{entryId: entry.id}"
|
||||||
|
@tags-updated="timestampAndUpdate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="c-snapshots c-ne__embeds">
|
<div class="c-snapshots c-ne__embeds">
|
||||||
@ -146,6 +147,8 @@ import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '..
|
|||||||
|
|
||||||
import Moment from 'moment';
|
import Moment from 'moment';
|
||||||
|
|
||||||
|
const UNKNOWN_USER = 'Unknown';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotebookEmbed,
|
NotebookEmbed,
|
||||||
@ -206,7 +209,8 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
targetKeyString,
|
targetKeyString,
|
||||||
entryId: this.entry.id
|
entryId: this.entry.id,
|
||||||
|
modified: this.entry.modified
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
createdOnTime() {
|
createdOnTime() {
|
||||||
@ -283,7 +287,7 @@ export default {
|
|||||||
await this.addNewEmbed(objectPath);
|
await this.addNewEmbed(objectPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('updateEntry', this.entry);
|
this.timestampAndUpdate();
|
||||||
},
|
},
|
||||||
findPositionInArray(array, id) {
|
findPositionInArray(array, id) {
|
||||||
let position = -1;
|
let position = -1;
|
||||||
@ -321,7 +325,7 @@ export default {
|
|||||||
// TODO: remove notebook snapshot object using object remove API
|
// TODO: remove notebook snapshot object using object remove API
|
||||||
this.entry.embeds.splice(embedPosition, 1);
|
this.entry.embeds.splice(embedPosition, 1);
|
||||||
|
|
||||||
this.$emit('updateEntry', this.entry);
|
this.timestampAndUpdate();
|
||||||
},
|
},
|
||||||
updateEmbed(newEmbed) {
|
updateEmbed(newEmbed) {
|
||||||
this.entry.embeds.some(e => {
|
this.entry.embeds.some(e => {
|
||||||
@ -333,6 +337,17 @@ export default {
|
|||||||
return found;
|
return found;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.timestampAndUpdate();
|
||||||
|
},
|
||||||
|
async timestampAndUpdate() {
|
||||||
|
const user = await this.openmct.user.getCurrentUser();
|
||||||
|
|
||||||
|
if (user === undefined) {
|
||||||
|
this.entry.modifiedBy = UNKNOWN_USER;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entry.modified = Date.now();
|
||||||
|
|
||||||
this.$emit('updateEntry', this.entry);
|
this.$emit('updateEntry', this.entry);
|
||||||
},
|
},
|
||||||
editingEntry() {
|
editingEntry() {
|
||||||
@ -342,7 +357,7 @@ export default {
|
|||||||
const value = $event.target.innerText;
|
const value = $event.target.innerText;
|
||||||
if (value !== this.entry.text && value.match(/\S/)) {
|
if (value !== this.entry.text && value.match(/\S/)) {
|
||||||
this.entry.text = value;
|
this.entry.text = value;
|
||||||
this.$emit('updateEntry', this.entry);
|
this.timestampAndUpdate();
|
||||||
} else {
|
} else {
|
||||||
this.$emit('cancelEdit');
|
this.$emit('cancelEdit');
|
||||||
}
|
}
|
||||||
|
@ -211,10 +211,17 @@ describe("Notebook plugin:", () => {
|
|||||||
|
|
||||||
describe("synchronization", () => {
|
describe("synchronization", () => {
|
||||||
|
|
||||||
|
let objectCloneToSyncFrom;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
objectCloneToSyncFrom = structuredClone(notebookViewObject);
|
||||||
|
objectCloneToSyncFrom.persisted = notebookViewObject.modified + 1;
|
||||||
|
});
|
||||||
|
|
||||||
it("updates an entry when another user modifies it", () => {
|
it("updates an entry when another user modifies it", () => {
|
||||||
expect(getEntryText(0).innerText).toBe("First Test Entry");
|
expect(getEntryText(0).innerText).toBe("First Test Entry");
|
||||||
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
|
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
|
||||||
objectProviderObserver(notebookViewObject);
|
objectProviderObserver(objectCloneToSyncFrom);
|
||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
return Vue.nextTick().then(() => {
|
||||||
expect(getEntryText(0).innerText).toBe("Modified entry text");
|
expect(getEntryText(0).innerText).toBe("Modified entry text");
|
||||||
@ -223,13 +230,13 @@ describe("Notebook plugin:", () => {
|
|||||||
|
|
||||||
it("shows new entry when another user adds one", () => {
|
it("shows new entry when another user adds one", () => {
|
||||||
expect(allNotebookEntryElements().length).toBe(2);
|
expect(allNotebookEntryElements().length).toBe(2);
|
||||||
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"].push({
|
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"].push({
|
||||||
"id": "entry-3",
|
"id": "entry-3",
|
||||||
"createdOn": 0,
|
"createdOn": 0,
|
||||||
"text": "Third Test Entry",
|
"text": "Third Test Entry",
|
||||||
"embeds": []
|
"embeds": []
|
||||||
});
|
});
|
||||||
objectProviderObserver(notebookViewObject);
|
objectProviderObserver(objectCloneToSyncFrom);
|
||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
return Vue.nextTick().then(() => {
|
||||||
expect(allNotebookEntryElements().length).toBe(3);
|
expect(allNotebookEntryElements().length).toBe(3);
|
||||||
@ -237,9 +244,9 @@ describe("Notebook plugin:", () => {
|
|||||||
});
|
});
|
||||||
it("removes an entry when another user removes one", () => {
|
it("removes an entry when another user removes one", () => {
|
||||||
expect(allNotebookEntryElements().length).toBe(2);
|
expect(allNotebookEntryElements().length).toBe(2);
|
||||||
let entries = notebookViewObject.configuration.entries["test-section-1"]["test-page-1"];
|
let entries = objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"];
|
||||||
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
|
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
|
||||||
objectProviderObserver(notebookViewObject);
|
objectProviderObserver(objectCloneToSyncFrom);
|
||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
return Vue.nextTick().then(() => {
|
||||||
expect(allNotebookEntryElements().length).toBe(1);
|
expect(allNotebookEntryElements().length).toBe(1);
|
||||||
@ -256,8 +263,8 @@ describe("Notebook plugin:", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(allNotebookPageElements().length).toBe(2);
|
expect(allNotebookPageElements().length).toBe(2);
|
||||||
notebookViewObject.configuration.sections[0].pages.push(newPage);
|
objectCloneToSyncFrom.configuration.sections[0].pages.push(newPage);
|
||||||
objectProviderObserver(notebookViewObject);
|
objectProviderObserver(objectCloneToSyncFrom);
|
||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
return Vue.nextTick().then(() => {
|
||||||
expect(allNotebookPageElements().length).toBe(3);
|
expect(allNotebookPageElements().length).toBe(3);
|
||||||
@ -267,8 +274,8 @@ describe("Notebook plugin:", () => {
|
|||||||
|
|
||||||
it("updates the notebook when a user removes a page", () => {
|
it("updates the notebook when a user removes a page", () => {
|
||||||
expect(allNotebookPageElements().length).toBe(2);
|
expect(allNotebookPageElements().length).toBe(2);
|
||||||
notebookViewObject.configuration.sections[0].pages.splice(0, 1);
|
objectCloneToSyncFrom.configuration.sections[0].pages.splice(0, 1);
|
||||||
objectProviderObserver(notebookViewObject);
|
objectProviderObserver(objectCloneToSyncFrom);
|
||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
return Vue.nextTick().then(() => {
|
||||||
expect(allNotebookPageElements().length).toBe(1);
|
expect(allNotebookPageElements().length).toBe(1);
|
||||||
@ -291,8 +298,8 @@ describe("Notebook plugin:", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(allNotebookSectionElements().length).toBe(2);
|
expect(allNotebookSectionElements().length).toBe(2);
|
||||||
notebookViewObject.configuration.sections.push(newSection);
|
objectCloneToSyncFrom.configuration.sections.push(newSection);
|
||||||
objectProviderObserver(notebookViewObject);
|
objectProviderObserver(objectCloneToSyncFrom);
|
||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
return Vue.nextTick().then(() => {
|
||||||
expect(allNotebookSectionElements().length).toBe(3);
|
expect(allNotebookSectionElements().length).toBe(3);
|
||||||
@ -301,8 +308,8 @@ describe("Notebook plugin:", () => {
|
|||||||
|
|
||||||
it("updates the notebook when a user removes a section", () => {
|
it("updates the notebook when a user removes a section", () => {
|
||||||
expect(allNotebookSectionElements().length).toBe(2);
|
expect(allNotebookSectionElements().length).toBe(2);
|
||||||
notebookViewObject.configuration.sections.splice(0, 1);
|
objectCloneToSyncFrom.configuration.sections.splice(0, 1);
|
||||||
objectProviderObserver(notebookViewObject);
|
objectProviderObserver(objectCloneToSyncFrom);
|
||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
return Vue.nextTick().then(() => {
|
||||||
expect(allNotebookSectionElements().length).toBe(1);
|
expect(allNotebookSectionElements().length).toBe(1);
|
||||||
|
@ -287,7 +287,7 @@ class CouchObjectProvider {
|
|||||||
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
|
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNotebookType(object)) {
|
if (isNotebookType(object) || object.type === 'annotation') {
|
||||||
//Temporary measure until object sync is supported for all object types
|
//Temporary measure until object sync is supported for all object types
|
||||||
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
|
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
|
||||||
this.objectQueue[key].updateRevision(response[REV]);
|
this.objectQueue[key].updateRevision(response[REV]);
|
||||||
|
@ -97,14 +97,17 @@ export default {
|
|||||||
this.tagsChanged(this.annotation.tags);
|
this.tagsChanged(this.annotation.tags);
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
|
},
|
||||||
|
annotationQuery: {
|
||||||
|
handler() {
|
||||||
|
this.unloadAnnotation();
|
||||||
|
this.loadAnnotation();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
mounted() {
|
||||||
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
|
this.loadAnnotation();
|
||||||
this.addAnnotationListener(this.annotation);
|
|
||||||
if (this.annotation && this.annotation.tags) {
|
|
||||||
this.tagsChanged(this.annotation.tags);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
if (this.removeTagsListener) {
|
if (this.removeTagsListener) {
|
||||||
@ -114,7 +117,23 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
addAnnotationListener(annotation) {
|
addAnnotationListener(annotation) {
|
||||||
if (annotation && !this.removeTagsListener) {
|
if (annotation && !this.removeTagsListener) {
|
||||||
this.removeTagsListener = this.openmct.objects.observe(annotation, 'tags', this.tagsChanged);
|
this.removeTagsListener = this.openmct.objects.observe(annotation, '*', (newAnnotation) => {
|
||||||
|
this.tagsChanged(newAnnotation.tags);
|
||||||
|
this.annotation = newAnnotation;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadAnnotation() {
|
||||||
|
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
|
||||||
|
this.addAnnotationListener(this.annotation);
|
||||||
|
if (this.annotation && this.annotation.tags) {
|
||||||
|
this.tagsChanged(this.annotation.tags);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unloadAnnotation() {
|
||||||
|
if (this.removeTagsListener) {
|
||||||
|
this.removeTagsListener();
|
||||||
|
this.removeTagsListener = undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tagsChanged(newTags) {
|
tagsChanged(newTags) {
|
||||||
@ -133,8 +152,11 @@ export default {
|
|||||||
this.addedTags.push(newTagValue);
|
this.addedTags.push(newTagValue);
|
||||||
this.userAddingTag = true;
|
this.userAddingTag = true;
|
||||||
},
|
},
|
||||||
tagRemoved(tagToRemove) {
|
async tagRemoved(tagToRemove) {
|
||||||
return this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||||
|
this.$emit('tags-updated');
|
||||||
|
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
async tagAdded(newTag) {
|
async tagAdded(newTag) {
|
||||||
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
|
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
|
||||||
@ -146,6 +168,8 @@ export default {
|
|||||||
|
|
||||||
this.tagsChanged(this.annotation.tags);
|
this.tagsChanged(this.annotation.tags);
|
||||||
this.userAddingTag = false;
|
this.userAddingTag = false;
|
||||||
|
|
||||||
|
this.$emit('tags-updated');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user