diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js index a8a3dd239b..6a650c6076 100644 --- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js @@ -44,6 +44,7 @@ async function createNotebookAndEntry(page, iterations = 1) { const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`; await page.locator(entryLocator).click(); await page.locator(entryLocator).fill(`Entry ${iteration}`); + await page.locator(entryLocator).press('Enter'); } return notebook; diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index f5031ec6df..1f23b305f4 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -193,23 +193,27 @@ export default class ObjectAPI { * @memberof module:openmct.ObjectProvider# * @param {string} key the key for the domain object to load * @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests + * @param {boolean} forceRemote defaults to false. If true, will skip cached and + * dirty/in-transaction objects use and the provider.get method * @returns {Promise} a promise which will resolve when the domain object * has been saved, or be rejected if it cannot be saved */ - get(identifier, abortSignal) { + get(identifier, abortSignal, forceRemote = false) { let keystring = this.makeKeyString(identifier); - if (this.cache[keystring] !== undefined) { - return this.cache[keystring]; - } + if (!forceRemote) { + if (this.cache[keystring] !== undefined) { + return this.cache[keystring]; + } - identifier = utils.parseKeyString(identifier); + identifier = utils.parseKeyString(identifier); - if (this.isTransactionActive()) { - let dirtyObject = this.transaction.getDirtyObject(identifier); + if (this.isTransactionActive()) { + let dirtyObject = this.transaction.getDirtyObject(identifier); - if (dirtyObject) { - return Promise.resolve(dirtyObject); + if (dirtyObject) { + return Promise.resolve(dirtyObject); + } } } @@ -391,7 +395,6 @@ export default class ObjectAPI { lastPersistedTime = domainObject.persisted; const persistedTime = Date.now(); this.#mutate(domainObject, 'persisted', persistedTime); - savedObjectPromise = provider.update(domainObject); } @@ -399,7 +402,7 @@ export default class ObjectAPI { savedObjectPromise.then(response => { savedResolve(response); }).catch((error) => { - if (lastPersistedTime !== undefined) { + if (!isNewObject) { this.#mutate(domainObject, 'persisted', lastPersistedTime); } @@ -412,11 +415,12 @@ export default class ObjectAPI { return result.catch(async (error) => { if (error instanceof this.errors.Conflict) { - this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`); + // Synchronized objects will resolve their own conflicts + if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) { + this.openmct.notifications.info(`Conflict detected while saving "${this.makeKeyString(domainObject.name)}", attempting to resolve`); + } else { + this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`); - // Synchronized objects will resolve their own conflicts, so - // bypass the refresh here and throw the error. - if (!this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) { if (this.isTransactionActive()) { this.endTransaction(); } diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index 91bdbebc71..5a029c51c6 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -50,7 +50,7 @@
+
{ Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => { - const remoteEntries = mutable.configuration.entries[sectionKey][pageKey]; + const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey]; const mergedEntries = [].concat(remoteEntries); let shouldMutate = false; @@ -110,8 +113,13 @@ function applyLocalEntries(mutable, entries, openmct) { }); if (shouldMutate) { - openmct.objects.mutate(mutable, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries); + shouldSave = true; + openmct.objects.mutate(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries); } }); }); + + if (shouldSave) { + return openmct.objects.save(remoteObject); + } } diff --git a/src/plugins/objectMigration/plugin.js b/src/plugins/objectMigration/plugin.js index 715d70418b..8c0db54079 100644 --- a/src/plugins/objectMigration/plugin.js +++ b/src/plugins/objectMigration/plugin.js @@ -36,8 +36,8 @@ export default function () { } let wrappedFunction = openmct.objects.get; - openmct.objects.get = function migrate(identifier) { - return wrappedFunction.apply(openmct.objects, [identifier]) + openmct.objects.get = function migrate() { + return wrappedFunction.apply(openmct.objects, [...arguments]) .then(function (object) { if (needsMigration(object)) { migrateObject(object) diff --git a/src/plugins/persistence/couch/CouchChangesFeed.js b/src/plugins/persistence/couch/CouchChangesFeed.js index 4547c6c9e4..86059841ca 100644 --- a/src/plugins/persistence/couch/CouchChangesFeed.js +++ b/src/plugins/persistence/couch/CouchChangesFeed.js @@ -28,6 +28,7 @@ connected = false; // stop listening for events couchEventSource.removeEventListener('message', self.onCouchMessage); + couchEventSource.close(); console.debug('🚪 Closed couch connection 🚪'); return; diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index 0b98d713a2..9ba083674a 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -96,8 +96,13 @@ class CouchObjectProvider { let keyString = this.openmct.objects.makeKeyString(objectIdentifier); //TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have. let observersForObject = this.observers[keyString]; + let isInTransaction = false; - if (observersForObject) { + if (this.openmct.objects.isTransactionActive()) { + isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier); + } + + if (observersForObject && !isInTransaction) { observersForObject.forEach(async (observer) => { const updatedObject = await this.get(objectIdentifier); if (this.isSynchronizedObject(updatedObject)) { @@ -219,7 +224,12 @@ class CouchObjectProvider { console.error(error.message); throw new Error(`CouchDB Error - No response"`); } else { - console.error(error.message); + if (body?.model && isNotebookOrAnnotationType(body.model)) { + // warn since we handle conflicts for notebooks + console.warn(error.message); + } else { + console.error(error.message); + } throw error; }