mirror of
https://github.com/nasa/openmct.git
synced 2024-12-30 18:06:59 +00:00
Support for remote mutation of Notebooks with Couch DB (#3887)
* Update notebook automatically when modified by another user * Don't persist selected and default page and section IDs on notebook object * Fixing object synchronization bugs * Adding unit tests * Synchronize notebooks AND plans * Removed observeEnabled flag Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
This commit is contained in:
parent
333e8b5583
commit
6755ef4641
@ -45,6 +45,8 @@ function ObjectAPI(typeRegistry, openmct) {
|
|||||||
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.interceptorRegistry = new InterceptorRegistry();
|
this.interceptorRegistry = new InterceptorRegistry();
|
||||||
|
|
||||||
|
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -404,11 +406,16 @@ ObjectAPI.prototype._toMutable = function (object) {
|
|||||||
let provider = this.getProvider(identifier);
|
let provider = this.getProvider(identifier);
|
||||||
|
|
||||||
if (provider !== undefined
|
if (provider !== undefined
|
||||||
&& provider.observe !== undefined) {
|
&& provider.observe !== undefined
|
||||||
|
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
|
||||||
let unobserve = provider.observe(identifier, (updatedModel) => {
|
let unobserve = provider.observe(identifier, (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.$refresh(updatedModel);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
mutableObject.$on('$destroy', () => {
|
mutableObject.$on('$_destroy', () => {
|
||||||
unobserve();
|
unobserve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -163,14 +163,22 @@ describe("The Object API", () => {
|
|||||||
key: 'test-key'
|
key: 'test-key'
|
||||||
},
|
},
|
||||||
name: 'test object',
|
name: 'test object',
|
||||||
|
type: 'notebook',
|
||||||
otherAttribute: 'other-attribute-value',
|
otherAttribute: 'other-attribute-value',
|
||||||
|
modified: 0,
|
||||||
|
persisted: 0,
|
||||||
objectAttribute: {
|
objectAttribute: {
|
||||||
embeddedObject: {
|
embeddedObject: {
|
||||||
embeddedKey: 'embedded-value'
|
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", [
|
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||||
"get",
|
"get",
|
||||||
"create",
|
"create",
|
||||||
@ -182,6 +190,8 @@ describe("The Object API", () => {
|
|||||||
mockProvider.observeObjectChanges.and.callFake(() => {
|
mockProvider.observeObjectChanges.and.callFake(() => {
|
||||||
callbacks[0](updatedTestObject);
|
callbacks[0](updatedTestObject);
|
||||||
callbacks.splice(0, 1);
|
callbacks.splice(0, 1);
|
||||||
|
|
||||||
|
return () => {};
|
||||||
});
|
});
|
||||||
mockProvider.observe.and.callFake((id, callback) => {
|
mockProvider.observe.and.callFake((id, callback) => {
|
||||||
if (callbacks.length === 0) {
|
if (callbacks.length === 0) {
|
||||||
@ -189,6 +199,8 @@ describe("The Object API", () => {
|
|||||||
} else {
|
} else {
|
||||||
callbacks[0] = callback;
|
callbacks[0] = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => {};
|
||||||
});
|
});
|
||||||
|
|
||||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<SearchResults v-if="search.length"
|
<SearchResults v-if="search.length"
|
||||||
ref="searchResults"
|
ref="searchResults"
|
||||||
:domain-object="internalDomainObject"
|
:domain-object="domainObject"
|
||||||
:results="searchResults"
|
:results="searchResults"
|
||||||
@changeSectionPage="changeSelectedSection"
|
@changeSectionPage="changeSelectedSection"
|
||||||
@updateEntries="updateEntries"
|
@updateEntries="updateEntries"
|
||||||
@ -43,15 +43,18 @@
|
|||||||
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
|
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
|
||||||
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
|
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
|
||||||
:default-page-id="defaultPageId"
|
:default-page-id="defaultPageId"
|
||||||
|
:selected-page-id="selectedPageId"
|
||||||
:default-section-id="defaultSectionId"
|
:default-section-id="defaultSectionId"
|
||||||
:domain-object="internalDomainObject"
|
:selected-section-id="selectedSectionId"
|
||||||
:page-title="internalDomainObject.configuration.pageTitle"
|
:domain-object="domainObject"
|
||||||
:section-title="internalDomainObject.configuration.sectionTitle"
|
:page-title="domainObject.configuration.pageTitle"
|
||||||
|
:section-title="domainObject.configuration.sectionTitle"
|
||||||
:sections="sections"
|
:sections="sections"
|
||||||
:selected-section="selectedSection"
|
|
||||||
:sidebar-covers-entries="sidebarCoversEntries"
|
:sidebar-covers-entries="sidebarCoversEntries"
|
||||||
@pagesChanged="pagesChanged"
|
@pagesChanged="pagesChanged"
|
||||||
|
@selectPage="selectPage"
|
||||||
@sectionsChanged="sectionsChanged"
|
@sectionsChanged="sectionsChanged"
|
||||||
|
@selectSection="selectSection"
|
||||||
@toggleNav="toggleNav"
|
@toggleNav="toggleNav"
|
||||||
/>
|
/>
|
||||||
<div class="c-notebook__page-view">
|
<div class="c-notebook__page-view">
|
||||||
@ -61,10 +64,10 @@
|
|||||||
></button>
|
></button>
|
||||||
<div class="c-notebook__page-view__path c-path">
|
<div class="c-notebook__page-view__path c-path">
|
||||||
<span class="c-notebook__path__section c-path__item">
|
<span class="c-notebook__path__section c-path__item">
|
||||||
{{ getSelectedSection() ? getSelectedSection().name : '' }}
|
{{ selectedSection ? selectedSection.name : '' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="c-notebook__path__page c-path__item">
|
<span class="c-notebook__path__page c-path__item">
|
||||||
{{ getSelectedPage() ? getSelectedPage().name : '' }}
|
{{ selectedPage ? selectedPage.name : '' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-notebook__page-view__controls">
|
<div class="c-notebook__page-view__controls">
|
||||||
@ -115,9 +118,9 @@
|
|||||||
<NotebookEntry v-for="entry in filteredAndSortedEntries"
|
<NotebookEntry v-for="entry in filteredAndSortedEntries"
|
||||||
:key="entry.id"
|
:key="entry.id"
|
||||||
:entry="entry"
|
:entry="entry"
|
||||||
:domain-object="internalDomainObject"
|
:domain-object="domainObject"
|
||||||
:selected-page="getSelectedPage()"
|
:selected-page="selectedPage"
|
||||||
:selected-section="getSelectedSection()"
|
:selected-section="selectedSection"
|
||||||
:read-only="false"
|
:read-only="false"
|
||||||
@deleteEntry="deleteEntry"
|
@deleteEntry="deleteEntry"
|
||||||
@updateEntry="updateEntry"
|
@updateEntry="updateEntry"
|
||||||
@ -152,14 +155,19 @@ export default {
|
|||||||
SearchResults,
|
SearchResults,
|
||||||
Sidebar
|
Sidebar
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject', 'snapshotContainer'],
|
inject: ['openmct', 'snapshotContainer'],
|
||||||
|
props: {
|
||||||
|
domainObject: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '',
|
selectedSectionId: this.getDefaultSectionId(),
|
||||||
defaultSectionId: getDefaultNotebook() ? getDefaultNotebook().section.id : '',
|
selectedPageId: this.getDefaultPageId(),
|
||||||
defaultSort: this.domainObject.configuration.defaultSort,
|
defaultSort: this.domainObject.configuration.defaultSort,
|
||||||
focusEntryId: null,
|
focusEntryId: null,
|
||||||
internalDomainObject: this.domainObject,
|
|
||||||
search: '',
|
search: '',
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
showTime: 0,
|
showTime: 0,
|
||||||
@ -168,9 +176,15 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
defaultPageId() {
|
||||||
|
return this.getDefaultPageId();
|
||||||
|
},
|
||||||
|
defaultSectionId() {
|
||||||
|
return this.getDefaultSectionId();
|
||||||
|
},
|
||||||
filteredAndSortedEntries() {
|
filteredAndSortedEntries() {
|
||||||
const filterTime = Date.now();
|
const filterTime = Date.now();
|
||||||
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
|
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
|
||||||
|
|
||||||
const hours = parseInt(this.showTime, 10);
|
const hours = parseInt(this.showTime, 10);
|
||||||
const filteredPageEntriesByTime = hours
|
const filteredPageEntriesByTime = hours
|
||||||
@ -185,22 +199,28 @@ export default {
|
|||||||
return this.getPages() || [];
|
return this.getPages() || [];
|
||||||
},
|
},
|
||||||
sections() {
|
sections() {
|
||||||
return this.internalDomainObject.configuration.sections || [];
|
return this.getSections();
|
||||||
},
|
},
|
||||||
selectedPage() {
|
selectedPage() {
|
||||||
const pages = this.getPages();
|
const pages = this.getPages();
|
||||||
if (!pages) {
|
const selectedPage = pages.find(page => page.id === this.selectedPageId);
|
||||||
return {};
|
|
||||||
|
if (selectedPage) {
|
||||||
|
return selectedPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pages.find(page => page.isSelected);
|
if (!selectedPage && !pages.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages[0];
|
||||||
},
|
},
|
||||||
selectedSection() {
|
selectedSection() {
|
||||||
if (!this.sections.length) {
|
if (!this.sections.length) {
|
||||||
return {};
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.sections.find(section => section.isSelected);
|
return this.sections.find(section => section.id === this.selectedSectionId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -210,16 +230,14 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.getSearchResults = debounce(this.getSearchResults, 500);
|
this.getSearchResults = debounce(this.getSearchResults, 500);
|
||||||
|
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
|
|
||||||
this.formatSidebar();
|
this.formatSidebar();
|
||||||
|
this.setSectionAndPageFromUrl();
|
||||||
|
|
||||||
window.addEventListener('orientationchange', this.formatSidebar);
|
window.addEventListener('orientationchange', this.formatSidebar);
|
||||||
window.addEventListener("hashchange", this.navigateToSectionPage, false);
|
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||||
this.openmct.router.on('change:params', this.changeSectionPage);
|
|
||||||
|
|
||||||
this.navigateToSectionPage();
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.unlisten) {
|
if (this.unlisten) {
|
||||||
@ -227,8 +245,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||||
window.removeEventListener("hashchange", this.navigateToSectionPage);
|
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||||
this.openmct.router.off('change:params', this.changeSectionPage);
|
|
||||||
},
|
},
|
||||||
updated: function () {
|
updated: function () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@ -284,14 +301,21 @@ export default {
|
|||||||
this.sectionsChanged({ sections });
|
this.sectionsChanged({ sections });
|
||||||
this.resetSearch();
|
this.resetSearch();
|
||||||
},
|
},
|
||||||
|
setSectionAndPageFromUrl() {
|
||||||
|
let sectionId = this.getSectionIdFromUrl() || this.selectedSectionId;
|
||||||
|
let pageId = this.getPageIdFromUrl() || this.selectedPageId;
|
||||||
|
|
||||||
|
this.selectSection(sectionId);
|
||||||
|
this.selectPage(pageId);
|
||||||
|
},
|
||||||
createNotebookStorageObject() {
|
createNotebookStorageObject() {
|
||||||
const notebookMeta = {
|
const notebookMeta = {
|
||||||
name: this.internalDomainObject.name,
|
name: this.domainObject.name,
|
||||||
identifier: this.internalDomainObject.identifier,
|
identifier: this.domainObject.identifier,
|
||||||
link: this.getLinktoNotebook()
|
link: this.getLinktoNotebook()
|
||||||
};
|
};
|
||||||
const page = this.getSelectedPage();
|
const page = this.selectedPage;
|
||||||
const section = this.getSelectedSection();
|
const section = this.selectedSection;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notebookMeta,
|
notebookMeta,
|
||||||
@ -300,8 +324,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
deleteEntry(entryId) {
|
deleteEntry(entryId) {
|
||||||
const self = this;
|
const entryPos = getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
|
||||||
const entryPos = getEntryPosById(entryId, this.internalDomainObject, this.selectedSection, this.selectedPage);
|
|
||||||
if (entryPos === -1) {
|
if (entryPos === -1) {
|
||||||
this.openmct.notifications.alert('Warning: unable to delete entry');
|
this.openmct.notifications.alert('Warning: unable to delete entry');
|
||||||
console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`);
|
console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`);
|
||||||
@ -317,9 +340,9 @@ export default {
|
|||||||
label: "Ok",
|
label: "Ok",
|
||||||
emphasis: true,
|
emphasis: true,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const entries = getNotebookEntries(self.internalDomainObject, self.selectedSection, self.selectedPage);
|
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||||
entries.splice(entryPos, 1);
|
entries.splice(entryPos, 1);
|
||||||
self.updateEntries(entries);
|
this.updateEntries(entries);
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -395,6 +418,37 @@ export default {
|
|||||||
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
|
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
|
||||||
this.sidebarCoversEntries = sidebarCoversEntries;
|
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() {
|
getDefaultNotebookObject() {
|
||||||
const oldNotebookStorage = getDefaultNotebook();
|
const oldNotebookStorage = getDefaultNotebook();
|
||||||
if (!oldNotebookStorage) {
|
if (!oldNotebookStorage) {
|
||||||
@ -423,14 +477,17 @@ export default {
|
|||||||
getSection(id) {
|
getSection(id) {
|
||||||
return this.sections.find(s => s.id === id);
|
return this.sections.find(s => s.id === id);
|
||||||
},
|
},
|
||||||
|
getSections() {
|
||||||
|
return this.domainObject.configuration.sections || [];
|
||||||
|
},
|
||||||
getSearchResults() {
|
getSearchResults() {
|
||||||
if (!this.search.length) {
|
if (!this.search.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = [];
|
const output = [];
|
||||||
const sections = this.internalDomainObject.configuration.sections;
|
const sections = this.domainObject.configuration.sections;
|
||||||
const entries = this.internalDomainObject.configuration.entries;
|
const entries = this.domainObject.configuration.entries;
|
||||||
const searchTextLower = this.search.toLowerCase();
|
const searchTextLower = this.search.toLowerCase();
|
||||||
const originalSearchText = this.search;
|
const originalSearchText = this.search;
|
||||||
let sectionTrackPageHit;
|
let sectionTrackPageHit;
|
||||||
@ -509,77 +566,25 @@ export default {
|
|||||||
this.searchResults = output;
|
this.searchResults = output;
|
||||||
},
|
},
|
||||||
getPages() {
|
getPages() {
|
||||||
const selectedSection = this.getSelectedSection();
|
const selectedSection = this.selectedSection;
|
||||||
if (!selectedSection || !selectedSection.pages.length) {
|
if (!selectedSection || !selectedSection.pages.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectedSection.pages;
|
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) {
|
newEntry(embed = null) {
|
||||||
this.resetSearch();
|
this.resetSearch();
|
||||||
const notebookStorage = this.createNotebookStorageObject();
|
const notebookStorage = this.createNotebookStorageObject();
|
||||||
this.updateDefaultNotebook(notebookStorage);
|
this.updateDefaultNotebook(notebookStorage);
|
||||||
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed);
|
const id = addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
|
||||||
this.focusEntryId = id;
|
this.focusEntryId = id;
|
||||||
},
|
},
|
||||||
orientationChange() {
|
orientationChange() {
|
||||||
this.formatSidebar();
|
this.formatSidebar();
|
||||||
},
|
},
|
||||||
pagesChanged({ pages = [], id = null}) {
|
pagesChanged({ pages = [], id = null}) {
|
||||||
const selectedSection = this.getSelectedSection();
|
const selectedSection = this.selectedSection;
|
||||||
if (!selectedSection) {
|
if (!selectedSection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -594,7 +599,6 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.sectionsChanged({ sections });
|
this.sectionsChanged({ sections });
|
||||||
this.updateDefaultNotebookPage(pages, id);
|
|
||||||
},
|
},
|
||||||
removeDefaultClass(domainObject) {
|
removeDefaultClass(domainObject) {
|
||||||
if (!domainObject) {
|
if (!domainObject) {
|
||||||
@ -613,10 +617,10 @@ export default {
|
|||||||
async updateDefaultNotebook(notebookStorage) {
|
async updateDefaultNotebook(notebookStorage) {
|
||||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
||||||
if (!defaultNotebookObject) {
|
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)) {
|
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
|
||||||
this.removeDefaultClass(defaultNotebookObject);
|
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) {
|
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
|
||||||
@ -636,7 +640,7 @@ export default {
|
|||||||
|
|
||||||
const notebookStorage = getDefaultNotebook();
|
const notebookStorage = getDefaultNotebook();
|
||||||
if (!notebookStorage
|
if (!notebookStorage
|
||||||
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
|
|| notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,7 +649,7 @@ export default {
|
|||||||
if (!page && defaultNotebookPage.id === id) {
|
if (!page && defaultNotebookPage.id === id) {
|
||||||
this.defaultSectionId = null;
|
this.defaultSectionId = null;
|
||||||
this.defaultPageId = null;
|
this.defaultPageId = null;
|
||||||
this.removeDefaultClass(this.internalDomainObject);
|
this.removeDefaultClass(this.domainObject);
|
||||||
clearDefaultNotebook();
|
clearDefaultNotebook();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -664,7 +668,7 @@ export default {
|
|||||||
|
|
||||||
const notebookStorage = getDefaultNotebook();
|
const notebookStorage = getDefaultNotebook();
|
||||||
if (!notebookStorage
|
if (!notebookStorage
|
||||||
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
|
|| notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -673,7 +677,7 @@ export default {
|
|||||||
if (!section && defaultNotebookSection.id === id) {
|
if (!section && defaultNotebookSection.id === id) {
|
||||||
this.defaultSectionId = null;
|
this.defaultSectionId = null;
|
||||||
this.defaultPageId = null;
|
this.defaultPageId = null;
|
||||||
this.removeDefaultClass(this.internalDomainObject);
|
this.removeDefaultClass(this.domainObject);
|
||||||
clearDefaultNotebook();
|
clearDefaultNotebook();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -686,50 +690,46 @@ export default {
|
|||||||
setDefaultNotebookSection(section);
|
setDefaultNotebookSection(section);
|
||||||
},
|
},
|
||||||
updateEntry(entry) {
|
updateEntry(entry) {
|
||||||
const entries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage);
|
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||||
const entryPos = getEntryPosById(entry.id, this.internalDomainObject, this.selectedSection, this.selectedPage);
|
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
|
||||||
entries[entryPos] = entry;
|
entries[entryPos] = entry;
|
||||||
|
|
||||||
this.updateEntries(entries);
|
this.updateEntries(entries);
|
||||||
},
|
},
|
||||||
updateEntries(entries) {
|
updateEntries(entries) {
|
||||||
const configuration = this.internalDomainObject.configuration;
|
const configuration = this.domainObject.configuration;
|
||||||
const notebookEntries = configuration.entries || {};
|
const notebookEntries = configuration.entries || {};
|
||||||
notebookEntries[this.selectedSection.id][this.selectedPage.id] = 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) {
|
getPageIdFromUrl() {
|
||||||
this.internalDomainObject = domainObject;
|
return this.openmct.router.getParams().pageId;
|
||||||
},
|
},
|
||||||
updateParams(sections) {
|
getSectionIdFromUrl() {
|
||||||
const selectedSection = sections.find(s => s.isSelected);
|
return this.openmct.router.getParams().sectionId;
|
||||||
if (!selectedSection) {
|
},
|
||||||
return;
|
syncUrlWithPageAndSection() {
|
||||||
}
|
|
||||||
|
|
||||||
const selectedPage = selectedSection.pages.find(p => p.isSelected);
|
|
||||||
if (!selectedPage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sectionId = selectedSection.id;
|
|
||||||
const pageId = selectedPage.id;
|
|
||||||
|
|
||||||
if (!sectionId || !pageId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.openmct.router.updateParams({
|
this.openmct.router.updateParams({
|
||||||
sectionId,
|
pageId: this.selectedPageId,
|
||||||
pageId
|
sectionId: this.selectedSectionId
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sectionsChanged({ sections, id = null }) {
|
sectionsChanged({ sections, id = null }) {
|
||||||
mutateObject(this.openmct, this.internalDomainObject, 'configuration.sections', sections);
|
mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections);
|
||||||
|
|
||||||
this.updateParams(sections);
|
|
||||||
this.updateDefaultNotebookSection(sections, id);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
>
|
>
|
||||||
<Page ref="pageComponent"
|
<Page ref="pageComponent"
|
||||||
:default-page-id="defaultPageId"
|
:default-page-id="defaultPageId"
|
||||||
|
:selected-page-id="selectedPageId"
|
||||||
:page="page"
|
:page="page"
|
||||||
:page-title="pageTitle"
|
:page-title="pageTitle"
|
||||||
@deletePage="deletePage"
|
@deletePage="deletePage"
|
||||||
@ -33,11 +34,13 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectedPageId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
required: true
|
||||||
return {};
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -66,7 +69,17 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
pages() {
|
||||||
|
if (!this.containsPage(this.selectedPageId)) {
|
||||||
|
this.selectPage(this.pages[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
containsPage(pageId) {
|
||||||
|
return this.pages.some(page => page.id === pageId);
|
||||||
|
},
|
||||||
deletePage(id) {
|
deletePage(id) {
|
||||||
const selectedSection = this.sections.find(s => s.isSelected);
|
const selectedSection = this.sections.find(s => s.isSelected);
|
||||||
const page = this.pages.find(p => p.id === id);
|
const page = this.pages.find(p => p.id === id);
|
||||||
@ -78,37 +91,29 @@ export default {
|
|||||||
const isPageSelected = selectedPage && selectedPage.id === id;
|
const isPageSelected = selectedPage && selectedPage.id === id;
|
||||||
const isPageDefault = defaultpage && defaultpage.id === id;
|
const isPageDefault = defaultpage && defaultpage.id === id;
|
||||||
const pages = this.pages.filter(s => s.id !== id);
|
const pages = this.pages.filter(s => s.id !== id);
|
||||||
|
let selectedPageId;
|
||||||
|
|
||||||
if (isPageSelected && defaultpage) {
|
if (isPageSelected && defaultpage) {
|
||||||
pages.forEach(s => {
|
pages.forEach(s => {
|
||||||
s.isSelected = false;
|
s.isSelected = false;
|
||||||
if (defaultpage && defaultpage.id === s.id) {
|
if (defaultpage && defaultpage.id === s.id) {
|
||||||
s.isSelected = true;
|
selectedPageId = s.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
|
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
|
||||||
pages[0].isSelected = true;
|
selectedPageId = pages[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('updatePage', {
|
this.$emit('updatePage', {
|
||||||
pages,
|
pages,
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
|
this.$emit('selectPage', selectedPageId);
|
||||||
},
|
},
|
||||||
selectPage(id) {
|
selectPage(id) {
|
||||||
const pages = this.pages.map(page => {
|
this.$emit('selectPage', id);
|
||||||
const isSelected = page.id === id;
|
|
||||||
page.isSelected = isSelected;
|
|
||||||
|
|
||||||
return page;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$emit('updatePage', {
|
|
||||||
pages,
|
|
||||||
id
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add test here for whether or not to toggle the nav
|
// Add test here for whether or not to toggle the nav
|
||||||
if (this.sidebarCoversEntries) {
|
if (this.sidebarCoversEntries) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="c-list__item js-list__item"
|
<div class="c-list__item js-list__item"
|
||||||
:class="[{ 'is-selected': page.isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
|
:class="[{ 'is-selected': isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
|
||||||
:data-id="page.id"
|
:data-id="page.id"
|
||||||
@click="selectPage"
|
@click="selectPage"
|
||||||
>
|
>
|
||||||
@ -29,6 +29,10 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectedPageId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
page: {
|
page: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@ -46,6 +50,11 @@ export default {
|
|||||||
removeActionString: `Delete ${this.pageTitle}`
|
removeActionString: `Delete ${this.pageTitle}`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isSelected() {
|
||||||
|
return this.selectedPageId === this.page.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
page(newPage) {
|
page(newPage) {
|
||||||
this.toggleContentEditable(newPage);
|
this.toggleContentEditable(newPage);
|
||||||
@ -73,7 +82,7 @@ export default {
|
|||||||
this.$emit('deletePage', this.page.id);
|
this.$emit('deletePage', this.page.id);
|
||||||
},
|
},
|
||||||
getRemoveDialog() {
|
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 = {
|
const options = {
|
||||||
name: this.removeActionString,
|
name: this.removeActionString,
|
||||||
callback: this.deletePage.bind(this),
|
callback: this.deletePage.bind(this),
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
:key="section.id"
|
:key="section.id"
|
||||||
class="c-list__item-h"
|
class="c-list__item-h"
|
||||||
>
|
>
|
||||||
<sectionComponent ref="sectionComponent"
|
<NotebookSection ref="sectionComponent"
|
||||||
:default-section-id="defaultSectionId"
|
:default-section-id="defaultSectionId"
|
||||||
|
:selected-section-id="selectedSectionId"
|
||||||
:section="section"
|
:section="section"
|
||||||
:section-title="sectionTitle"
|
:section-title="sectionTitle"
|
||||||
@deleteSection="deleteSection"
|
@deleteSection="deleteSection"
|
||||||
@ -19,11 +20,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { deleteNotebookEntries } from '../utils/notebook-entries';
|
import { deleteNotebookEntries } from '../utils/notebook-entries';
|
||||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||||
import sectionComponent from './SectionComponent.vue';
|
import SectionComponent from './SectionComponent.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
sectionComponent
|
NotebookSection: SectionComponent
|
||||||
},
|
},
|
||||||
inject: ['openmct'],
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
@ -33,6 +34,10 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectedSectionId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
@ -53,12 +58,22 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
sections() {
|
||||||
|
if (!this.containsSection(this.selectedSectionId)) {
|
||||||
|
this.selectSection(this.sections[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
containsSection(sectionId) {
|
||||||
|
return this.sections.some(section => section.id === sectionId);
|
||||||
|
},
|
||||||
deleteSection(id) {
|
deleteSection(id) {
|
||||||
const section = this.sections.find(s => s.id === id);
|
const section = this.sections.find(s => s.id === id);
|
||||||
deleteNotebookEntries(this.openmct, this.domainObject, section);
|
deleteNotebookEntries(this.openmct, this.domainObject, section);
|
||||||
|
|
||||||
const selectedSection = this.sections.find(s => s.isSelected);
|
const selectedSection = this.sections.find(s => s.id === this.selectedSectionId);
|
||||||
const defaultNotebook = getDefaultNotebook();
|
const defaultNotebook = getDefaultNotebook();
|
||||||
const defaultSection = defaultNotebook && defaultNotebook.section;
|
const defaultSection = defaultNotebook && defaultNotebook.section;
|
||||||
const isSectionSelected = selectedSection && selectedSection.id === id;
|
const isSectionSelected = selectedSection && selectedSection.id === id;
|
||||||
@ -83,18 +98,8 @@ export default {
|
|||||||
id
|
id
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
selectSection(id, newSections) {
|
selectSection(id) {
|
||||||
const currentSections = newSections || this.sections;
|
this.$emit('selectSection', id);
|
||||||
const sections = currentSections.map(section => {
|
|
||||||
const isSelected = section.id === id;
|
|
||||||
section.isSelected = isSelected;
|
|
||||||
|
|
||||||
return section;
|
|
||||||
});
|
|
||||||
this.$emit('updateSection', {
|
|
||||||
sections,
|
|
||||||
id
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
updateSection(newSection) {
|
updateSection(newSection) {
|
||||||
const id = newSection.id;
|
const id = newSection.id;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="c-list__item js-list__item"
|
<div class="c-list__item js-list__item"
|
||||||
:class="[{ 'is-selected': section.isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]"
|
:class="[{ 'is-selected': isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]"
|
||||||
:data-id="section.id"
|
:data-id="section.id"
|
||||||
@click="selectSection"
|
@click="selectSection"
|
||||||
>
|
>
|
||||||
@ -13,9 +13,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PopupMenu from './PopupMenu.vue';
|
import PopupMenu from './PopupMenu.vue';
|
||||||
import RemoveDialog from '../utils/removeDialog';
|
import RemoveDialog from '../utils/removeDialog';
|
||||||
@ -32,6 +29,10 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectedSectionId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
section: {
|
section: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@ -49,6 +50,11 @@ export default {
|
|||||||
removeActionString: `Delete ${this.sectionTitle}`
|
removeActionString: `Delete ${this.sectionTitle}`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isSelected() {
|
||||||
|
return this.selectedSectionId === this.section.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
section(newSection) {
|
section(newSection) {
|
||||||
this.toggleContentEditable(newSection);
|
this.toggleContentEditable(newSection);
|
||||||
@ -76,7 +82,7 @@ export default {
|
|||||||
this.$emit('deleteSection', this.section.id);
|
this.$emit('deleteSection', this.section.id);
|
||||||
},
|
},
|
||||||
getRemoveDialog() {
|
getRemoveDialog() {
|
||||||
const message = 'This action will delete this section and all of its pages and entries. Do you want to continue?';
|
const message = 'Other users may be editing entries in this section, and deleting it is permanent. Do you want to continue?';
|
||||||
const options = {
|
const options = {
|
||||||
name: this.removeActionString,
|
name: this.removeActionString,
|
||||||
callback: this.deleteSection.bind(this),
|
callback: this.deleteSection.bind(this),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="c-sidebar c-drawer c-drawer--align-left">
|
<div class="c-sidebar c-drawer c-drawer--align-left">
|
||||||
<div class="c-sidebar__pane">
|
<div class="c-sidebar__pane js-sidebar-sections">
|
||||||
<div class="c-sidebar__header-w">
|
<div class="c-sidebar__header-w">
|
||||||
<div class="c-sidebar__header">
|
<div class="c-sidebar__header">
|
||||||
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
|
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
|
||||||
@ -15,14 +15,16 @@
|
|||||||
</button>
|
</button>
|
||||||
<SectionCollection class="c-sidebar__contents"
|
<SectionCollection class="c-sidebar__contents"
|
||||||
:default-section-id="defaultSectionId"
|
:default-section-id="defaultSectionId"
|
||||||
|
:selected-section-id="selectedSectionId"
|
||||||
:domain-object="domainObject"
|
:domain-object="domainObject"
|
||||||
:sections="sections"
|
:sections="sections"
|
||||||
:section-title="sectionTitle"
|
:section-title="sectionTitle"
|
||||||
@updateSection="sectionsChanged"
|
@updateSection="sectionsChanged"
|
||||||
|
@selectSection="selectSection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-sidebar__pane">
|
<div class="c-sidebar__pane js-sidebar-pages">
|
||||||
<div class="c-sidebar__header-w">
|
<div class="c-sidebar__header-w">
|
||||||
<div class="c-sidebar__header">
|
<div class="c-sidebar__header">
|
||||||
<span class="c-sidebar__header-label">{{ pageTitle }}</span>
|
<span class="c-sidebar__header-label">{{ pageTitle }}</span>
|
||||||
@ -42,6 +44,7 @@
|
|||||||
<PageCollection ref="pageCollection"
|
<PageCollection ref="pageCollection"
|
||||||
class="c-sidebar__contents"
|
class="c-sidebar__contents"
|
||||||
:default-page-id="defaultPageId"
|
:default-page-id="defaultPageId"
|
||||||
|
:selected-page-id="selectedPageId"
|
||||||
:domain-object="domainObject"
|
:domain-object="domainObject"
|
||||||
:pages="pages"
|
:pages="pages"
|
||||||
:sections="sections"
|
:sections="sections"
|
||||||
@ -49,6 +52,7 @@
|
|||||||
:page-title="pageTitle"
|
:page-title="pageTitle"
|
||||||
@toggleNav="toggleNav"
|
@toggleNav="toggleNav"
|
||||||
@updatePage="pagesChanged"
|
@updatePage="pagesChanged"
|
||||||
|
@selectPage="selectPage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -73,12 +77,24 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectedPageId: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
defaultSectionId: {
|
defaultSectionId: {
|
||||||
type: String,
|
type: String,
|
||||||
default() {
|
default() {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectedSectionId: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
@ -113,7 +129,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
pages() {
|
pages() {
|
||||||
const selectedSection = this.sections.find(section => section.isSelected);
|
const selectedSection = this.sections.find(section => section.id === this.selectedSectionId);
|
||||||
|
|
||||||
return selectedSection && selectedSection.pages || [];
|
return selectedSection && selectedSection.pages || [];
|
||||||
}
|
}
|
||||||
@ -144,6 +160,7 @@ export default {
|
|||||||
pages,
|
pages,
|
||||||
id: newPage.id
|
id: newPage.id
|
||||||
});
|
});
|
||||||
|
this.$emit('selectPage', newPage.id);
|
||||||
},
|
},
|
||||||
addSection() {
|
addSection() {
|
||||||
const newSection = this.createNewSection();
|
const newSection = this.createNewSection();
|
||||||
@ -153,6 +170,8 @@ export default {
|
|||||||
sections,
|
sections,
|
||||||
id: newSection.id
|
id: newSection.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$emit('selectSection', newSection.id);
|
||||||
},
|
},
|
||||||
addNewPage(page) {
|
addNewPage(page) {
|
||||||
const pages = this.pages.map(p => {
|
const pages = this.pages.map(p => {
|
||||||
@ -208,6 +227,12 @@ export default {
|
|||||||
id
|
id
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
selectPage(pageId) {
|
||||||
|
this.$emit('selectPage', pageId);
|
||||||
|
},
|
||||||
|
selectSection(sectionId) {
|
||||||
|
this.$emit('selectSection', sectionId);
|
||||||
|
},
|
||||||
sectionsChanged({ sections, id }) {
|
sectionsChanged({ sections, id }) {
|
||||||
this.$emit('sectionsChanged', {
|
this.$emit('sectionsChanged', {
|
||||||
sections,
|
sections,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export const NOTEBOOK_TYPE = 'notebook';
|
||||||
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
|
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
|
||||||
export const NOTEBOOK_DEFAULT = 'DEFAULT';
|
export const NOTEBOOK_DEFAULT = 'DEFAULT';
|
||||||
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
|
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
|
||||||
|
@ -2,18 +2,17 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
|
|||||||
import Notebook from './components/Notebook.vue';
|
import Notebook from './components/Notebook.vue';
|
||||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||||
import SnapshotContainer from './snapshot-container';
|
import SnapshotContainer from './snapshot-container';
|
||||||
|
import {NOTEBOOK_TYPE} from './notebook-constants';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
let installed = false;
|
|
||||||
|
|
||||||
export default function NotebookPlugin() {
|
export default function NotebookPlugin() {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
if (installed) {
|
if (openmct._NOTEBOOK_PLUGIN_INSTALLED) {
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
openmct._NOTEBOOK_PLUGIN_INSTALLED = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
installed = true;
|
|
||||||
|
|
||||||
openmct.actions.register(new CopyToNotebookAction(openmct));
|
openmct.actions.register(new CopyToNotebookAction(openmct));
|
||||||
|
|
||||||
const notebookType = {
|
const notebookType = {
|
||||||
@ -84,7 +83,7 @@ export default function NotebookPlugin() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
openmct.types.addType('notebook', notebookType);
|
openmct.types.addType(NOTEBOOK_TYPE, notebookType);
|
||||||
|
|
||||||
const snapshotContainer = new SnapshotContainer(openmct);
|
const snapshotContainer = new SnapshotContainer(openmct);
|
||||||
const notebookSnapshotIndicator = new Vue ({
|
const notebookSnapshotIndicator = new Vue ({
|
||||||
@ -123,10 +122,14 @@ export default function NotebookPlugin() {
|
|||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject,
|
|
||||||
snapshotContainer
|
snapshotContainer
|
||||||
},
|
},
|
||||||
template: '<Notebook></Notebook>'
|
data() {
|
||||||
|
return {
|
||||||
|
domainObject
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: '<Notebook :domain-object="domainObject"></Notebook>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -21,29 +21,32 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
|
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
|
||||||
import NotebookPlugin from './plugin';
|
import notebookPlugin from './plugin';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
let openmct;
|
describe("Notebook plugin:", () => {
|
||||||
let notebookDefinition;
|
let openmct;
|
||||||
let notebookPlugin;
|
let notebookDefinition;
|
||||||
let element;
|
let element;
|
||||||
let child;
|
let child;
|
||||||
let appHolder;
|
let appHolder;
|
||||||
|
let objectProviderObserver;
|
||||||
|
|
||||||
const notebookDomainObject = {
|
let notebookDomainObject;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
notebookDomainObject = {
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'notebook',
|
key: 'notebook',
|
||||||
namespace: ''
|
namespace: 'test-namespace'
|
||||||
},
|
},
|
||||||
type: 'notebook'
|
type: 'notebook'
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("Notebook plugin:", () => {
|
|
||||||
beforeAll(done => {
|
|
||||||
appHolder = document.createElement('div');
|
appHolder = document.createElement('div');
|
||||||
appHolder.style.width = '640px';
|
appHolder.style.width = '640px';
|
||||||
appHolder.style.height = '480px';
|
appHolder.style.height = '480px';
|
||||||
|
document.body.appendChild(appHolder);
|
||||||
|
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
|
|
||||||
@ -51,19 +54,16 @@ describe("Notebook plugin:", () => {
|
|||||||
child = document.createElement('div');
|
child = document.createElement('div');
|
||||||
element.appendChild(child);
|
element.appendChild(child);
|
||||||
|
|
||||||
notebookPlugin = new NotebookPlugin();
|
openmct.install(notebookPlugin());
|
||||||
openmct.install(notebookPlugin);
|
|
||||||
|
|
||||||
notebookDefinition = openmct.types.get('notebook').definition;
|
notebookDefinition = openmct.types.get('notebook').definition;
|
||||||
notebookDefinition.initialize(notebookDomainObject);
|
notebookDefinition.initialize(notebookDomainObject);
|
||||||
|
|
||||||
openmct.on('start', done);
|
openmct.on('start', done);
|
||||||
openmct.start(appHolder);
|
openmct.start(appHolder);
|
||||||
|
|
||||||
document.body.append(appHolder);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterEach(() => {
|
||||||
appHolder.remove();
|
appHolder.remove();
|
||||||
|
|
||||||
return resetApplicationState(openmct);
|
return resetApplicationState(openmct);
|
||||||
@ -80,39 +80,96 @@ describe("Notebook plugin:", () => {
|
|||||||
describe("Notebook view:", () => {
|
describe("Notebook view:", () => {
|
||||||
let notebookViewProvider;
|
let notebookViewProvider;
|
||||||
let notebookView;
|
let notebookView;
|
||||||
|
let notebookViewObject;
|
||||||
|
let mutableNotebookObject;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const notebookViewObject = {
|
notebookViewObject = {
|
||||||
...notebookDomainObject,
|
...notebookDomainObject,
|
||||||
id: "test-object",
|
id: "test-object",
|
||||||
name: 'Notebook',
|
name: 'Notebook',
|
||||||
configuration: {
|
configuration: {
|
||||||
defaultSort: 'oldest',
|
defaultSort: 'oldest',
|
||||||
entries: {},
|
entries: {
|
||||||
|
"test-section-1": {
|
||||||
|
"test-page-1": [{
|
||||||
|
"id": "entry-0",
|
||||||
|
"createdOn": 0,
|
||||||
|
"text": "First Test Entry",
|
||||||
|
"embeds": []
|
||||||
|
}, {
|
||||||
|
"id": "entry-1",
|
||||||
|
"createdOn": 0,
|
||||||
|
"text": "Second Test Entry",
|
||||||
|
"embeds": []
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
pageTitle: 'Page',
|
pageTitle: 'Page',
|
||||||
sections: [],
|
sections: [{
|
||||||
|
"id": "test-section-1",
|
||||||
|
"isDefault": false,
|
||||||
|
"isSelected": false,
|
||||||
|
"name": "Test Section",
|
||||||
|
"pages": [{
|
||||||
|
"id": "test-page-1",
|
||||||
|
"isDefault": false,
|
||||||
|
"isSelected": false,
|
||||||
|
"name": "Test Page 1",
|
||||||
|
"pageTitle": "Page"
|
||||||
|
}, {
|
||||||
|
"id": "test-page-2",
|
||||||
|
"isDefault": false,
|
||||||
|
"isSelected": false,
|
||||||
|
"name": "Test Page 2",
|
||||||
|
"pageTitle": "Page"
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
"id": "test-section-2",
|
||||||
|
"isDefault": false,
|
||||||
|
"isSelected": false,
|
||||||
|
"name": "Test Section 2",
|
||||||
|
"pages": [{
|
||||||
|
"id": "test-page-3",
|
||||||
|
"isDefault": false,
|
||||||
|
"isSelected": false,
|
||||||
|
"name": "Test Page 3",
|
||||||
|
"pageTitle": "Page"
|
||||||
|
}]
|
||||||
|
}],
|
||||||
sectionTitle: 'Section',
|
sectionTitle: 'Section',
|
||||||
type: 'General'
|
type: 'General'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
|
||||||
|
'get',
|
||||||
|
'create',
|
||||||
|
'update',
|
||||||
|
'observe'
|
||||||
|
]);
|
||||||
|
|
||||||
const notebookObject = {
|
const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);
|
||||||
name: 'Notebook View',
|
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'notebook-vue');
|
||||||
key: 'notebook-vue',
|
|
||||||
creatable: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const applicableViews = openmct.objectViews.get(notebookViewObject, []);
|
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
|
||||||
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === notebookObject.key);
|
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||||
notebookView = notebookViewProvider.view(notebookViewObject);
|
testObjectProvider.observe.and.returnValue(() => {});
|
||||||
|
|
||||||
|
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
|
||||||
|
mutableNotebookObject = mutableObject;
|
||||||
|
objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1];
|
||||||
|
|
||||||
|
notebookView = notebookViewProvider.view(mutableNotebookObject);
|
||||||
notebookView.show(child);
|
notebookView.show(child);
|
||||||
|
|
||||||
return Vue.nextTick();
|
return Vue.nextTick();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
notebookView.destroy();
|
notebookView.destroy();
|
||||||
|
openmct.objects.destroyMutable(mutableNotebookObject);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("provides notebook view", () => {
|
it("provides notebook view", () => {
|
||||||
@ -133,6 +190,114 @@ describe("Notebook plugin:", () => {
|
|||||||
|
|
||||||
expect(hasMajorElements).toBe(true);
|
expect(hasMajorElements).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders a row for each entry", () => {
|
||||||
|
const notebookEntryElements = element.querySelectorAll('.c-notebook__entry');
|
||||||
|
const firstEntryText = getEntryText(0);
|
||||||
|
expect(notebookEntryElements.length).toBe(2);
|
||||||
|
expect(firstEntryText.innerText).toBe('First Test Entry');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("synchronization", () => {
|
||||||
|
|
||||||
|
it("updates an entry when another user modifies it", () => {
|
||||||
|
expect(getEntryText(0).innerText).toBe("First Test Entry");
|
||||||
|
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
|
||||||
|
objectProviderObserver(notebookViewObject);
|
||||||
|
|
||||||
|
return Vue.nextTick().then(() => {
|
||||||
|
expect(getEntryText(0).innerText).toBe("Modified entry text");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows new entry when another user adds one", () => {
|
||||||
|
expect(allNotebookEntryElements().length).toBe(2);
|
||||||
|
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"].push({
|
||||||
|
"id": "entry-3",
|
||||||
|
"createdOn": 0,
|
||||||
|
"text": "Third Test Entry",
|
||||||
|
"embeds": []
|
||||||
|
});
|
||||||
|
objectProviderObserver(notebookViewObject);
|
||||||
|
|
||||||
|
return Vue.nextTick().then(() => {
|
||||||
|
expect(allNotebookEntryElements().length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("removes an entry when another user removes one", () => {
|
||||||
|
expect(allNotebookEntryElements().length).toBe(2);
|
||||||
|
let entries = notebookViewObject.configuration.entries["test-section-1"]["test-page-1"];
|
||||||
|
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
|
||||||
|
objectProviderObserver(notebookViewObject);
|
||||||
|
|
||||||
|
return Vue.nextTick().then(() => {
|
||||||
|
expect(allNotebookEntryElements().length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the notebook when a user adds a page", () => {
|
||||||
|
const newPage = {
|
||||||
|
"id": "test-page-4",
|
||||||
|
"isDefault": false,
|
||||||
|
"isSelected": false,
|
||||||
|
"name": "Test Page 4",
|
||||||
|
"pageTitle": "Page"
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(allNotebookPageElements().length).toBe(2);
|
||||||
|
notebookViewObject.configuration.sections[0].pages.push(newPage);
|
||||||
|
objectProviderObserver(notebookViewObject);
|
||||||
|
|
||||||
|
return Vue.nextTick().then(() => {
|
||||||
|
expect(allNotebookPageElements().length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the notebook when a user removes a page", () => {
|
||||||
|
expect(allNotebookPageElements().length).toBe(2);
|
||||||
|
notebookViewObject.configuration.sections[0].pages.splice(0, 1);
|
||||||
|
objectProviderObserver(notebookViewObject);
|
||||||
|
|
||||||
|
return Vue.nextTick().then(() => {
|
||||||
|
expect(allNotebookPageElements().length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the notebook when a user adds a section", () => {
|
||||||
|
const newSection = {
|
||||||
|
"id": "test-section-3",
|
||||||
|
"isDefault": false,
|
||||||
|
"isSelected": false,
|
||||||
|
"name": "Test Section 3",
|
||||||
|
"pages": [{
|
||||||
|
"id": "test-page-4",
|
||||||
|
"isDefault": false,
|
||||||
|
"isSelected": false,
|
||||||
|
"name": "Test Page 4",
|
||||||
|
"pageTitle": "Page"
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(allNotebookSectionElements().length).toBe(2);
|
||||||
|
notebookViewObject.configuration.sections.push(newSection);
|
||||||
|
objectProviderObserver(notebookViewObject);
|
||||||
|
|
||||||
|
return Vue.nextTick().then(() => {
|
||||||
|
expect(allNotebookSectionElements().length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the notebook when a user removes a section", () => {
|
||||||
|
expect(allNotebookSectionElements().length).toBe(2);
|
||||||
|
notebookViewObject.configuration.sections.splice(0, 1);
|
||||||
|
objectProviderObserver(notebookViewObject);
|
||||||
|
|
||||||
|
return Vue.nextTick().then(() => {
|
||||||
|
expect(allNotebookSectionElements().length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Notebook Snapshots view:", () => {
|
describe("Notebook Snapshots view:", () => {
|
||||||
@ -147,16 +312,22 @@ describe("Notebook plugin:", () => {
|
|||||||
button.dispatchEvent(clickEvent);
|
button.dispatchEvent(clickEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeEach(() => {
|
||||||
snapshotIndicator = openmct.indicators.indicatorObjects
|
snapshotIndicator = openmct.indicators.indicatorObjects
|
||||||
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
|
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
|
||||||
|
|
||||||
element.append(snapshotIndicator);
|
element.append(snapshotIndicator);
|
||||||
|
|
||||||
return Vue.nextTick();
|
return Vue.nextTick().then(() => {
|
||||||
|
drawerElement = document.querySelector('.l-shell__drawer');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterEach(() => {
|
||||||
|
if (drawerElement) {
|
||||||
|
drawerElement.classList.remove('is-expanded');
|
||||||
|
}
|
||||||
|
|
||||||
snapshotIndicator.remove();
|
snapshotIndicator.remove();
|
||||||
snapshotIndicator = undefined;
|
snapshotIndicator = undefined;
|
||||||
|
|
||||||
@ -166,16 +337,6 @@ describe("Notebook plugin:", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
drawerElement = document.querySelector('.l-shell__drawer');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
if (drawerElement) {
|
|
||||||
drawerElement.classList.remove('is-expanded');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has Snapshots indicator", () => {
|
it("has Snapshots indicator", () => {
|
||||||
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
|
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
|
||||||
expect(hasSnapshotIndicator).toBe(true);
|
expect(hasSnapshotIndicator).toBe(true);
|
||||||
@ -219,4 +380,20 @@ describe("Notebook plugin:", () => {
|
|||||||
expect(snapshotsText).toBe('Notebook Snapshots');
|
expect(snapshotsText).toBe('Notebook Snapshots');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getEntryText(entryNumber) {
|
||||||
|
return element.querySelectorAll('.c-notebook__entry .c-ne__text')[entryNumber];
|
||||||
|
}
|
||||||
|
|
||||||
|
function allNotebookEntryElements() {
|
||||||
|
return element.querySelectorAll('.c-notebook__entry');
|
||||||
|
}
|
||||||
|
|
||||||
|
function allNotebookSectionElements() {
|
||||||
|
return element.querySelectorAll('.js-sidebar-sections .js-list__item');
|
||||||
|
}
|
||||||
|
|
||||||
|
function allNotebookPageElements() {
|
||||||
|
return element.querySelectorAll('.js-sidebar-pages .js-list__item');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
import CouchDocument from "./CouchDocument";
|
import CouchDocument from "./CouchDocument";
|
||||||
import CouchObjectQueue from "./CouchObjectQueue";
|
import CouchObjectQueue from "./CouchObjectQueue";
|
||||||
|
import NOTEBOOK_TYPE from '../../notebook/notebook-constants.js';
|
||||||
|
|
||||||
const REV = "_rev";
|
const REV = "_rev";
|
||||||
const ID = "_id";
|
const ID = "_id";
|
||||||
@ -29,24 +30,14 @@ const HEARTBEAT = 50000;
|
|||||||
const ALL_DOCS = "_all_docs?include_docs=true";
|
const ALL_DOCS = "_all_docs?include_docs=true";
|
||||||
|
|
||||||
export default class CouchObjectProvider {
|
export default class CouchObjectProvider {
|
||||||
// options {
|
|
||||||
// url: couchdb url,
|
|
||||||
// disableObserve: disable auto feed from couchdb to keep objects in sync,
|
|
||||||
// filter: selector to find objects to sync in couchdb
|
|
||||||
// }
|
|
||||||
constructor(openmct, options, namespace) {
|
constructor(openmct, options, namespace) {
|
||||||
options = this._normalize(options);
|
options = this._normalize(options);
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.url = options.url;
|
this.url = options.url;
|
||||||
this.namespace = namespace;
|
this.namespace = namespace;
|
||||||
this.objectQueue = {};
|
this.objectQueue = {};
|
||||||
this.observeEnabled = options.disableObserve !== true;
|
|
||||||
this.observers = {};
|
this.observers = {};
|
||||||
this.batchIds = [];
|
this.batchIds = [];
|
||||||
|
|
||||||
if (this.observeEnabled) {
|
|
||||||
this.observeObjectChanges(options.filter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//backwards compatibility, options used to be a url. Now it's an object
|
//backwards compatibility, options used to be a url. Now it's an object
|
||||||
@ -133,8 +124,12 @@ export default class CouchObjectProvider {
|
|||||||
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
|
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (object.type === NOTEBOOK_TYPE) {
|
||||||
|
//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.
|
||||||
|
this.objectQueue[key].updateRevision(response[REV]);
|
||||||
|
} else if (!this.objectQueue[key].pending) {
|
||||||
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
|
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
|
||||||
if (!this.objectQueue[key].pending) {
|
|
||||||
this.objectQueue[key].updateRevision(response[REV]);
|
this.objectQueue[key].updateRevision(response[REV]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,49 +308,63 @@ export default class CouchObjectProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
observe(identifier, callback) {
|
observe(identifier, callback) {
|
||||||
if (!this.observeEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyString = this.openmct.objects.makeKeyString(identifier);
|
const keyString = this.openmct.objects.makeKeyString(identifier);
|
||||||
this.observers[keyString] = this.observers[keyString] || [];
|
this.observers[keyString] = this.observers[keyString] || [];
|
||||||
this.observers[keyString].push(callback);
|
this.observers[keyString].push(callback);
|
||||||
|
|
||||||
|
if (!this.isObservingObjectChanges()) {
|
||||||
|
this.observeObjectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
|
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
|
||||||
|
if (this.observers[keyString].length === 0) {
|
||||||
|
delete this.observers[keyString];
|
||||||
|
if (Object.keys(this.observers).length === 0) {
|
||||||
|
this.stopObservingObjectChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
isObservingObjectChanges() {
|
||||||
* @private
|
return this.stopObservingObjectChanges !== undefined;
|
||||||
*/
|
|
||||||
abortGetChanges() {
|
|
||||||
if (this.controller) {
|
|
||||||
this.controller.abort();
|
|
||||||
this.controller = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async observeObjectChanges(filter) {
|
async observeObjectChanges() {
|
||||||
let intermediateResponse = this.getIntermediateResponse();
|
|
||||||
|
|
||||||
if (!this.observeEnabled) {
|
|
||||||
intermediateResponse.reject('Observe for changes is disabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const signal = controller.signal;
|
const signal = controller.signal;
|
||||||
|
let filter = {selector: {}};
|
||||||
|
|
||||||
if (this.controller) {
|
if (this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.length > 1) {
|
||||||
this.abortGetChanges();
|
filter.selector.$or = this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES
|
||||||
|
.map(type => {
|
||||||
|
return {
|
||||||
|
'model': {
|
||||||
|
type
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
filter.selector.model = {
|
||||||
|
type: this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES[0]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.controller = controller;
|
let error = false;
|
||||||
|
|
||||||
|
if (typeof this.stopObservingObjectChanges === 'function') {
|
||||||
|
this.stopObservingObjectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopObservingObjectChanges = () => {
|
||||||
|
controller.abort();
|
||||||
|
delete this.stopObservingObjectChanges;
|
||||||
|
};
|
||||||
|
|
||||||
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
||||||
// style=main_only returns only the current winning revision of the document
|
// style=main_only returns only the current winning revision of the document
|
||||||
let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`;
|
let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`;
|
||||||
@ -374,14 +383,20 @@ export default class CouchObjectProvider {
|
|||||||
},
|
},
|
||||||
body
|
body
|
||||||
});
|
});
|
||||||
const reader = response.body.getReader();
|
|
||||||
let completed = false;
|
|
||||||
|
|
||||||
while (!completed) {
|
let reader;
|
||||||
|
|
||||||
|
if (response.body === undefined) {
|
||||||
|
error = true;
|
||||||
|
} else {
|
||||||
|
reader = response.body.getReader();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!error) {
|
||||||
const {done, value} = await reader.read();
|
const {done, value} = await reader.read();
|
||||||
//done is true when we lose connection with the provider
|
//done is true when we lose connection with the provider
|
||||||
if (done) {
|
if (done) {
|
||||||
completed = true;
|
error = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
@ -414,11 +429,9 @@ export default class CouchObjectProvider {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//We're done receiving from the provider. No more chunks.
|
if (error && Object.keys(this.observers).length > 0) {
|
||||||
intermediateResponse.resolve(true);
|
this.observeObjectChanges();
|
||||||
|
}
|
||||||
return intermediateResponse.promise;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,8 +27,6 @@ import {
|
|||||||
|
|
||||||
describe('the plugin', () => {
|
describe('the plugin', () => {
|
||||||
let openmct;
|
let openmct;
|
||||||
let element;
|
|
||||||
let child;
|
|
||||||
let provider;
|
let provider;
|
||||||
let testPath = '/test/db';
|
let testPath = '/test/db';
|
||||||
let options;
|
let options;
|
||||||
@ -36,6 +34,8 @@ describe('the plugin', () => {
|
|||||||
let mockDomainObject;
|
let mockDomainObject;
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
|
spyOnBuiltins(['fetch'], window);
|
||||||
|
|
||||||
mockDomainObject = {
|
mockDomainObject = {
|
||||||
identifier: {
|
identifier: {
|
||||||
namespace: '',
|
namespace: '',
|
||||||
@ -51,8 +51,6 @@ describe('the plugin', () => {
|
|||||||
};
|
};
|
||||||
openmct = createOpenMct(false);
|
openmct = createOpenMct(false);
|
||||||
|
|
||||||
spyOnBuiltins(['fetch'], window);
|
|
||||||
|
|
||||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||||
mockIdentifierService = jasmine.createSpyObj(
|
mockIdentifierService = jasmine.createSpyObj(
|
||||||
'identifierService',
|
'identifierService',
|
||||||
@ -70,10 +68,6 @@ describe('the plugin', () => {
|
|||||||
|
|
||||||
openmct.types.addType('mock-type', {creatable: true});
|
openmct.types.addType('mock-type', {creatable: true});
|
||||||
|
|
||||||
element = document.createElement('div');
|
|
||||||
child = document.createElement('div');
|
|
||||||
element.appendChild(child);
|
|
||||||
|
|
||||||
openmct.on('start', done);
|
openmct.on('start', done);
|
||||||
openmct.startHeadless();
|
openmct.startHeadless();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user