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 @@
@@ -29,6 +29,10 @@ export default {
return '';
}
},
+ selectedPageId: {
+ type: String,
+ required: true
+ },
page: {
type: Object,
required: true
@@ -46,6 +50,11 @@ export default {
removeActionString: `Delete ${this.pageTitle}`
};
},
+ computed: {
+ isSelected() {
+ return this.selectedPageId === this.page.id;
+ }
+ },
watch: {
page(newPage) {
this.toggleContentEditable(newPage);
@@ -73,7 +82,7 @@ export default {
this.$emit('deletePage', this.page.id);
},
getRemoveDialog() {
- const message = 'This action will delete this page and all of its entries. Do you want to continue?';
+ const message = 'Other users may be editing entries in this page, and deleting it is permanent. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deletePage.bind(this),
diff --git a/src/plugins/notebook/components/SectionCollection.vue b/src/plugins/notebook/components/SectionCollection.vue
index cd819ba9ff..a8df17b1f0 100644
--- a/src/plugins/notebook/components/SectionCollection.vue
+++ b/src/plugins/notebook/components/SectionCollection.vue
@@ -4,13 +4,14 @@
:key="section.id"
class="c-list__item-h"
>
-
@@ -19,11 +20,11 @@