diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index d383a2e746..a9a6eca150 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -45,6 +45,8 @@ function ObjectAPI(typeRegistry, openmct) { this.rootProvider = new RootObjectProvider(this.rootRegistry); this.cache = {}; this.interceptorRegistry = new InterceptorRegistry(); + + this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan']; } /** @@ -404,11 +406,16 @@ ObjectAPI.prototype._toMutable = function (object) { let provider = this.getProvider(identifier); if (provider !== undefined - && provider.observe !== undefined) { + && provider.observe !== undefined + && this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) { let unobserve = provider.observe(identifier, (updatedModel) => { - mutableObject.$refresh(updatedModel); + if (updatedModel.persisted > mutableObject.modified) { + //Don't replace with a stale model. This can happen on slow connections when multiple mutations happen + //in rapid succession and intermediate persistence states are returned by the observe function. + mutableObject.$refresh(updatedModel); + } }); - mutableObject.$on('$destroy', () => { + mutableObject.$on('$_destroy', () => { unobserve(); }); } diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js index 2564145624..697d61a58c 100644 --- a/src/api/objects/ObjectAPISpec.js +++ b/src/api/objects/ObjectAPISpec.js @@ -163,14 +163,22 @@ describe("The Object API", () => { key: 'test-key' }, name: 'test object', + type: 'notebook', otherAttribute: 'other-attribute-value', + modified: 0, + persisted: 0, objectAttribute: { embeddedObject: { embeddedKey: 'embedded-value' } } }; - updatedTestObject = Object.assign({otherAttribute: 'changed-attribute-value'}, testObject); + updatedTestObject = Object.assign({ + otherAttribute: 'changed-attribute-value' + }, testObject); + updatedTestObject.modified = 1; + updatedTestObject.persisted = 1; + mockProvider = jasmine.createSpyObj("mock provider", [ "get", "create", @@ -182,6 +190,8 @@ describe("The Object API", () => { mockProvider.observeObjectChanges.and.callFake(() => { callbacks[0](updatedTestObject); callbacks.splice(0, 1); + + return () => {}; }); mockProvider.observe.and.callFake((id, callback) => { if (callbacks.length === 0) { @@ -189,6 +199,8 @@ describe("The Object API", () => { } else { callbacks[0] = callback; } + + return () => {}; }); objectAPI.addProvider(TEST_NAMESPACE, mockProvider); diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index 72441d7333..6d42d7ae7a 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -31,7 +31,7 @@
@@ -61,10 +64,10 @@ >
- {{ getSelectedSection() ? getSelectedSection().name : '' }} + {{ selectedSection ? selectedSection.name : '' }} - {{ getSelectedPage() ? getSelectedPage().name : '' }} + {{ selectedPage ? selectedPage.name : '' }}
@@ -115,9 +118,9 @@ page.id === this.selectedPageId); + + if (selectedPage) { + return selectedPage; } - return pages.find(page => page.isSelected); + if (!selectedPage && !pages.length) { + return undefined; + } + + return pages[0]; }, selectedSection() { if (!this.sections.length) { - return {}; + return null; } - return this.sections.find(section => section.isSelected); + return this.sections.find(section => section.id === this.selectedSectionId); } }, watch: { @@ -210,16 +230,14 @@ export default { }, beforeMount() { this.getSearchResults = debounce(this.getSearchResults, 500); + this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100); }, mounted() { - this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject); this.formatSidebar(); + this.setSectionAndPageFromUrl(); window.addEventListener('orientationchange', this.formatSidebar); - window.addEventListener("hashchange", this.navigateToSectionPage, false); - this.openmct.router.on('change:params', this.changeSectionPage); - - this.navigateToSectionPage(); + window.addEventListener('hashchange', this.setSectionAndPageFromUrl); }, beforeDestroy() { if (this.unlisten) { @@ -227,8 +245,7 @@ export default { } window.removeEventListener('orientationchange', this.formatSidebar); - window.removeEventListener("hashchange", this.navigateToSectionPage); - this.openmct.router.off('change:params', this.changeSectionPage); + window.removeEventListener('hashchange', this.setSectionAndPageFromUrl); }, updated: function () { this.$nextTick(() => { @@ -284,14 +301,21 @@ export default { this.sectionsChanged({ sections }); this.resetSearch(); }, + setSectionAndPageFromUrl() { + let sectionId = this.getSectionIdFromUrl() || this.selectedSectionId; + let pageId = this.getPageIdFromUrl() || this.selectedPageId; + + this.selectSection(sectionId); + this.selectPage(pageId); + }, createNotebookStorageObject() { const notebookMeta = { - name: this.internalDomainObject.name, - identifier: this.internalDomainObject.identifier, + name: this.domainObject.name, + identifier: this.domainObject.identifier, link: this.getLinktoNotebook() }; - const page = this.getSelectedPage(); - const section = this.getSelectedSection(); + const page = this.selectedPage; + const section = this.selectedSection; return { notebookMeta, @@ -300,8 +324,7 @@ export default { }; }, deleteEntry(entryId) { - const self = this; - const entryPos = getEntryPosById(entryId, this.internalDomainObject, this.selectedSection, this.selectedPage); + const entryPos = getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage); if (entryPos === -1) { this.openmct.notifications.alert('Warning: unable to delete entry'); console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`); @@ -317,9 +340,9 @@ export default { label: "Ok", emphasis: true, callback: () => { - const entries = getNotebookEntries(self.internalDomainObject, self.selectedSection, self.selectedPage); + const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage); entries.splice(entryPos, 1); - self.updateEntries(entries); + this.updateEntries(entries); dialog.dismiss(); } }, @@ -395,6 +418,37 @@ export default { const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout); this.sidebarCoversEntries = sidebarCoversEntries; }, + getDefaultPageId() { + let defaultPageId; + + if (this.isDefaultNotebook()) { + defaultPageId = getDefaultNotebook().page.id; + } else { + const firstSection = this.getSections()[0]; + defaultPageId = firstSection && firstSection.pages[0].id; + } + + return defaultPageId; + }, + isDefaultNotebook() { + const defaultNotebook = getDefaultNotebook(); + const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.notebookMeta.identifier; + + return defaultNotebookIdentifier !== null + && this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, this.domainObject.identifier); + }, + getDefaultSectionId() { + let defaultSectionId; + + if (this.isDefaultNotebook()) { + defaultSectionId = getDefaultNotebook().section.id; + } else { + const firstSection = this.getSections()[0]; + defaultSectionId = firstSection && firstSection.id; + } + + return defaultSectionId; + }, getDefaultNotebookObject() { const oldNotebookStorage = getDefaultNotebook(); if (!oldNotebookStorage) { @@ -423,14 +477,17 @@ export default { getSection(id) { return this.sections.find(s => s.id === id); }, + getSections() { + return this.domainObject.configuration.sections || []; + }, getSearchResults() { if (!this.search.length) { return []; } const output = []; - const sections = this.internalDomainObject.configuration.sections; - const entries = this.internalDomainObject.configuration.entries; + const sections = this.domainObject.configuration.sections; + const entries = this.domainObject.configuration.entries; const searchTextLower = this.search.toLowerCase(); const originalSearchText = this.search; let sectionTrackPageHit; @@ -509,77 +566,25 @@ export default { this.searchResults = output; }, getPages() { - const selectedSection = this.getSelectedSection(); + const selectedSection = this.selectedSection; if (!selectedSection || !selectedSection.pages.length) { return []; } return selectedSection.pages; }, - getSelectedPage() { - const pages = this.getPages(); - if (!pages) { - return null; - } - - const selectedPage = pages.find(page => page.isSelected); - if (selectedPage) { - return selectedPage; - } - - if (!selectedPage && !pages.length) { - return null; - } - - pages[0].isSelected = true; - - return pages[0]; - }, - getSelectedSection() { - if (!this.sections.length) { - return null; - } - - return this.sections.find(section => section.isSelected); - }, - navigateToSectionPage() { - let { pageId, sectionId } = this.openmct.router.getParams(); - - if (!pageId || !sectionId) { - sectionId = this.selectedSection.id; - pageId = this.selectedPage.id; - } - - const sections = this.sections.map(s => { - s.isSelected = false; - if (s.id === sectionId) { - s.isSelected = true; - s.pages.forEach(p => p.isSelected = (p.id === pageId)); - } - - return s; - }); - - const selectedSectionId = this.selectedSection && this.selectedSection.id; - const selectedPageId = this.selectedPage && this.selectedPage.id; - if (selectedPageId === pageId && selectedSectionId === sectionId) { - return; - } - - this.sectionsChanged({ sections }); - }, newEntry(embed = null) { this.resetSearch(); const notebookStorage = this.createNotebookStorageObject(); this.updateDefaultNotebook(notebookStorage); - const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed); + const id = addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed); this.focusEntryId = id; }, orientationChange() { this.formatSidebar(); }, pagesChanged({ pages = [], id = null}) { - const selectedSection = this.getSelectedSection(); + const selectedSection = this.selectedSection; if (!selectedSection) { return; } @@ -594,7 +599,6 @@ export default { }); this.sectionsChanged({ sections }); - this.updateDefaultNotebookPage(pages, id); }, removeDefaultClass(domainObject) { if (!domainObject) { @@ -613,10 +617,10 @@ export default { async updateDefaultNotebook(notebookStorage) { const defaultNotebookObject = await this.getDefaultNotebookObject(); if (!defaultNotebookObject) { - setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject); + setDefaultNotebook(this.openmct, notebookStorage, this.domainObject); } else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) { this.removeDefaultClass(defaultNotebookObject); - setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject); + setDefaultNotebook(this.openmct, notebookStorage, this.domainObject); } if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) { @@ -636,7 +640,7 @@ export default { const notebookStorage = getDefaultNotebook(); if (!notebookStorage - || notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) { + || notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) { return; } @@ -645,7 +649,7 @@ export default { if (!page && defaultNotebookPage.id === id) { this.defaultSectionId = null; this.defaultPageId = null; - this.removeDefaultClass(this.internalDomainObject); + this.removeDefaultClass(this.domainObject); clearDefaultNotebook(); return; @@ -664,7 +668,7 @@ export default { const notebookStorage = getDefaultNotebook(); if (!notebookStorage - || notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) { + || notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) { return; } @@ -673,7 +677,7 @@ export default { if (!section && defaultNotebookSection.id === id) { this.defaultSectionId = null; this.defaultPageId = null; - this.removeDefaultClass(this.internalDomainObject); + this.removeDefaultClass(this.domainObject); clearDefaultNotebook(); return; @@ -686,50 +690,46 @@ export default { setDefaultNotebookSection(section); }, updateEntry(entry) { - const entries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage); - const entryPos = getEntryPosById(entry.id, this.internalDomainObject, this.selectedSection, this.selectedPage); + const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage); + const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage); entries[entryPos] = entry; this.updateEntries(entries); }, updateEntries(entries) { - const configuration = this.internalDomainObject.configuration; + const configuration = this.domainObject.configuration; const notebookEntries = configuration.entries || {}; notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries; - mutateObject(this.openmct, this.internalDomainObject, 'configuration.entries', notebookEntries); + mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries); }, - updateInternalDomainObject(domainObject) { - this.internalDomainObject = domainObject; + getPageIdFromUrl() { + return this.openmct.router.getParams().pageId; }, - updateParams(sections) { - const selectedSection = sections.find(s => s.isSelected); - if (!selectedSection) { - return; - } - - const selectedPage = selectedSection.pages.find(p => p.isSelected); - if (!selectedPage) { - return; - } - - const sectionId = selectedSection.id; - const pageId = selectedPage.id; - - if (!sectionId || !pageId) { - return; - } - + getSectionIdFromUrl() { + return this.openmct.router.getParams().sectionId; + }, + syncUrlWithPageAndSection() { this.openmct.router.updateParams({ - sectionId, - pageId + pageId: this.selectedPageId, + sectionId: this.selectedSectionId }); }, sectionsChanged({ sections, id = null }) { - mutateObject(this.openmct, this.internalDomainObject, 'configuration.sections', sections); - - this.updateParams(sections); + mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections); this.updateDefaultNotebookSection(sections, id); + }, + selectPage(pageId) { + this.selectedPageId = pageId; + this.syncUrlWithPageAndSection(); + }, + selectSection(sectionId) { + this.selectedSectionId = sectionId; + + const defaultPageId = this.selectedSection.pages[0].id; + this.selectPage(defaultPageId); + + this.syncUrlWithPageAndSection(); } } }; diff --git a/src/plugins/notebook/components/PageCollection.vue b/src/plugins/notebook/components/PageCollection.vue index 2734d20db1..79d72d718a 100644 --- a/src/plugins/notebook/components/PageCollection.vue +++ b/src/plugins/notebook/components/PageCollection.vue @@ -6,6 +6,7 @@ > page.id === pageId); + }, deletePage(id) { const selectedSection = this.sections.find(s => s.isSelected); const page = this.pages.find(p => p.id === id); @@ -78,37 +91,29 @@ export default { const isPageSelected = selectedPage && selectedPage.id === id; const isPageDefault = defaultpage && defaultpage.id === id; const pages = this.pages.filter(s => s.id !== id); + let selectedPageId; if (isPageSelected && defaultpage) { pages.forEach(s => { s.isSelected = false; if (defaultpage && defaultpage.id === s.id) { - s.isSelected = true; + selectedPageId = s.id; } }); } if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) { - pages[0].isSelected = true; + selectedPageId = pages[0].id; } this.$emit('updatePage', { pages, id }); + this.$emit('selectPage', selectedPageId); }, selectPage(id) { - const pages = this.pages.map(page => { - const isSelected = page.id === id; - page.isSelected = isSelected; - - return page; - }); - - this.$emit('updatePage', { - pages, - id - }); + this.$emit('selectPage', id); // Add test here for whether or not to toggle the nav if (this.sidebarCoversEntries) { diff --git a/src/plugins/notebook/components/PageComponent.vue b/src/plugins/notebook/components/PageComponent.vue index 1f2a5d4176..ed87bc2eb6 100644 --- a/src/plugins/notebook/components/PageComponent.vue +++ b/src/plugins/notebook/components/PageComponent.vue @@ -1,6 +1,6 @@