From ce463babfff0f800d4eac6bbbb1ea9d838d0dce9 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 30 Sep 2022 19:32:11 +0200 Subject: [PATCH] 5734 synchronization for new tags on notebook entries (#5763) * trying this again * wip * wip * wip * one annotation per tag * fixed too many events firing * syncing works mostly * syncing properly across existing annotations * search with multiple tags * resolve conflicts between different tag editors * resolve conflicts * fix annotation tests * combine search results * modify tests * prevent infinite loop creating annotation * add modified and deleted * revert index checkin * change to standard couch deleted flag * revert throwing of error * resolve conflict issues * work in progress, but load annotations once from notebook * works to add * attempt 1 * wip * last changes * listening works, though still getting conflicts * rename to annotationLastCreated * use local mutable again * works with new tags syncing * listeners wont fire if modification is null * clean up code * fixed local search * cleaned up log messages * remove on more log * add e2e test for network traffic * lint * change to use good old for each * add some local variables for clarity * Update src/api/objects/ObjectAPI.js Co-authored-by: Jesse Mazzella * Update src/api/objects/ObjectAPI.js Co-authored-by: Jesse Mazzella * Update src/plugins/notebook/components/Notebook.vue Co-authored-by: Jesse Mazzella * press enter for last entry * add test explanation of numbers * fix spread typo * add some nice jsdoc * throw some errors * use really small integer instead * remove unneeded binding * make method public and jsdoc it * use mutables * clean up tests * clean up tests * use aria labels for tests * add some proper tsdoc to annotation api * add undelete test Co-authored-by: John Hill Co-authored-by: Jesse Mazzella --- docs/src/index.md | 4 +- .../notebook/notebookWithCouchDB.e2e.spec.js | 232 ++++++++++++++++++ .../plugins/notebook/tags.e2e.spec.js | 21 +- src/api/annotation/AnnotationAPI.js | 122 +++++++-- src/api/annotation/AnnotationAPISpec.js | 43 ++-- src/api/composition/CompositionCollection.js | 2 +- src/api/objects/InMemorySearchProvider.js | 58 +---- src/api/objects/InMemorySearchWorker.js | 31 --- src/api/objects/ObjectAPI.js | 43 ++-- src/api/objects/ObjectAPISpec.js | 2 +- src/plugins/notebook/components/Notebook.vue | 55 ++++- .../notebook/components/NotebookEntry.vue | 18 +- src/plugins/notebook/components/Sidebar.vue | 2 + .../monkeyPatchObjectAPIForNotebooks.js | 61 ++++- src/plugins/notebook/notebook-constants.js | 9 + .../operatorStatus/AbstractStatusIndicator.js | 2 +- .../persistence/couch/CouchObjectProvider.js | 6 +- .../persistence/couch/CouchSearchProvider.js | 44 +--- src/selection/Selection.js | 2 +- src/ui/components/tags/TagEditor.vue | 100 ++++---- src/ui/components/tags/TagSelection.vue | 5 +- webpack.dev.js | 2 +- 22 files changed, 568 insertions(+), 296 deletions(-) create mode 100644 e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js diff --git a/docs/src/index.md b/docs/src/index.md index 3166ae6019..52781a3739 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -15,8 +15,8 @@ ## Sections - * The [API](api/) document is generated from inline documentation - using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and + * The [API](api/) uses inline documentation + using [TypeScript](https://www.typescriptlang.org) and some legacy [JSDoc](https://jsdoc.app/). It describes the JavaScript objects and functions that make up the software platform. * The [Development Process](process/) document describes the diff --git a/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js new file mode 100644 index 0000000000..4edb1ff28c --- /dev/null +++ b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js @@ -0,0 +1,232 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB. +*/ + +const { test, expect } = require('../../../../baseFixtures'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); + +test.describe('Notebook Network Request Inspection @couchdb', () => { + let testNotebook; + test.beforeEach(async ({ page }) => { + //Navigate to baseURL + await page.goto('./', { waitUntil: 'networkidle' }); + + // Create Notebook + testNotebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + name: "TestNotebook" + }); + }); + + test('Inspect Notebook Entry Network Requests', async ({ page }) => { + // Expand sidebar + await page.locator('.c-notebook__toggle-nav-button').click(); + + // Collect all request events to count and assert after notebook action + let addingNotebookElementsRequests = []; + page.on('request', (request) => addingNotebookElementsRequests.push(request)); + + let [notebookUrlRequest, allDocsRequest] = await Promise.all([ + // Waits for the next request with the specified url + page.waitForRequest(`**/openmct/${testNotebook.uuid}`), + page.waitForRequest('**/openmct/_all_docs?include_docs=true'), + // Triggers the request + page.click('[aria-label="Add Page"]'), + // Ensures that there are no other network requests + page.waitForLoadState('networkidle') + ]); + // Assert that only two requests are made + // Network Requests are: + // 1) The actual POST to create the page + // 2) The shared worker event from 👆 request + expect(addingNotebookElementsRequests.length).toBe(2); + + // Assert on request object + expect(notebookUrlRequest.postDataJSON().metadata.name).toBe('TestNotebook'); + expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified); + expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid); + + // Add an entry + // Network Requests are: + // 1) The actual POST to create the entry + // 2) The shared worker event from 👆 POST request + addingNotebookElementsRequests = []; + await page.locator('text=To start a new entry, click here or drag and drop any object').click(); + await page.locator('[aria-label="Notebook Entry Input"]').click(); + await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`); + await page.waitForLoadState('networkidle'); + expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2); + + // Add some tags + // Network Requests are for each tag creation are: + // 1) Getting the original path of the parent object + // 2) Getting the original path of the grandparent object (recursive call) + // 3) Creating the annotation/tag object + // 4) The shared worker event from 👆 POST request + // 5) Mutate notebook domain object's annotationModified property + // 6) The shared worker event from 👆 POST request + // 7) Notebooks fetching new annotations due to annotationModified changed + // 8) The update of the notebook domain's object's modified property + // 9) The shared worker event from 👆 POST request + // 10) Entry is timestamped + // 11) The shared worker event from 👆 POST request + + addingNotebookElementsRequests = []; + await page.hover(`button:has-text("Add Tag")`); + await page.locator(`button:has-text("Add Tag")`).click(); + await page.locator('[placeholder="Type to select tag"]').click(); + await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); + await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")'); + page.waitForLoadState('networkidle'); + expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11); + + addingNotebookElementsRequests = []; + await page.hover(`button:has-text("Add Tag")`); + await page.locator(`button:has-text("Add Tag")`).click(); + await page.locator('[placeholder="Type to select tag"]').click(); + await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click(); + await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")'); + page.waitForLoadState('networkidle'); + expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11); + + addingNotebookElementsRequests = []; + await page.hover(`button:has-text("Add Tag")`); + await page.locator(`button:has-text("Add Tag")`).click(); + await page.locator('[placeholder="Type to select tag"]').click(); + await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); + await page.waitForSelector('[aria-label="Tag"]:has-text("Science")'); + page.waitForLoadState('networkidle'); + expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11); + + // Delete all the tags + // Network requests are: + // 1) Send POST to mutate _delete property to true on annotation with tag + // 2) The shared worker event from 👆 POST request + // 3) Timestamp update on entry + // 4) The shared worker event from 👆 POST request + // This happens for 3 tags so 12 requests + addingNotebookElementsRequests = []; + await page.hover('[aria-label="Tag"]:has-text("Driving")'); + await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click(); + await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")', {state: 'hidden'}); + await page.hover('[aria-label="Tag"]:has-text("Drilling")'); + await page.locator('[aria-label="Tag"]:has-text("Drilling") ~ .c-completed-tag-deletion').click(); + await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")', {state: 'hidden'}); + page.hover('[aria-label="Tag"]:has-text("Science")'); + await page.locator('[aria-label="Tag"]:has-text("Science") ~ .c-completed-tag-deletion').click(); + await page.waitForSelector('[aria-label="Tag"]:has-text("Science")', {state: 'hidden'}); + page.waitForLoadState('networkidle'); + expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(12); + + // Add two more pages + await page.click('[aria-label="Add Page"]'); + await page.click('[aria-label="Add Page"]'); + + // Add three entries + await page.locator('text=To start a new entry, click here or drag and drop any object').click(); + await page.locator('[aria-label="Notebook Entry Input"]').click(); + await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`); + + await page.locator('text=To start a new entry, click here or drag and drop any object').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`); + + await page.locator('text=To start a new entry, click here or drag and drop any object').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`); + + // Add three tags + await page.hover(`button:has-text("Add Tag") >> nth=2`); + await page.locator(`button:has-text("Add Tag") >> nth=2`).click(); + await page.locator('[placeholder="Type to select tag"]').click(); + await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); + await page.waitForSelector('[aria-label="Tag"]:has-text("Science")'); + + await page.hover(`button:has-text("Add Tag") >> nth=2`); + await page.locator(`button:has-text("Add Tag") >> nth=2`).click(); + await page.locator('[placeholder="Type to select tag"]').click(); + await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click(); + await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")'); + + await page.hover(`button:has-text("Add Tag") >> nth=2`); + await page.locator(`button:has-text("Add Tag") >> nth=2`).click(); + await page.locator('[placeholder="Type to select tag"]').click(); + await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); + await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")'); + page.waitForLoadState('networkidle'); + + // Add a fourth entry + // Network requests are: + // 1) Send POST to add new entry + // 2) The shared worker event from 👆 POST request + // 3) Timestamp update on entry + // 4) The shared worker event from 👆 POST request + addingNotebookElementsRequests = []; + await page.locator('text=To start a new entry, click here or drag and drop any object').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').fill(`Fourth Entry`); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').press('Enter'); + page.waitForLoadState('networkidle'); + + expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4); + + // Add a fifth entry + // Network requests are: + // 1) Send POST to add new entry + // 2) The shared worker event from 👆 POST request + // 3) Timestamp update on entry + // 4) The shared worker event from 👆 POST request + addingNotebookElementsRequests = []; + await page.locator('text=To start a new entry, click here or drag and drop any object').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').fill(`Fifth Entry`); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').press('Enter'); + page.waitForLoadState('networkidle'); + + expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4); + + // Add a sixth entry + // 1) Send POST to add new entry + // 2) The shared worker event from 👆 POST request + // 3) Timestamp update on entry + // 4) The shared worker event from 👆 POST request + addingNotebookElementsRequests = []; + await page.locator('text=To start a new entry, click here or drag and drop any object').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').click(); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').fill(`Sixth Entry`); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').press('Enter'); + page.waitForLoadState('networkidle'); + + expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4); + }); +}); + +// Try to reduce indeterminism of browser requests by only returning fetch requests. +// Filter out preflight CORS, fetching stylesheets, page icons, etc. that can occur during tests +function filterNonFetchRequests(requests) { + return requests.filter(request => { + return (request.resourceType() === 'fetch'); + }); +} diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js index 11533197c8..e2cda186a6 100644 --- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js @@ -81,10 +81,8 @@ test.describe('Tagging in Notebooks @addInit', () => { test('Can load tags', async ({ page }) => { await createNotebookAndEntry(page); - // Click text=To start a new entry, click here or drag and drop any object await page.locator('button:has-text("Add Tag")').click(); - // Click [placeholder="Type to select tag"] await page.locator('[placeholder="Type to select tag"]').click(); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science"); @@ -97,9 +95,7 @@ test.describe('Tagging in Notebooks @addInit', () => { await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); - // Click button:has-text("Add Tag") await page.locator('button:has-text("Add Tag")').click(); - // Click [placeholder="Type to select tag"] await page.locator('[placeholder="Type to select tag"]').click(); await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science"); @@ -108,39 +104,31 @@ test.describe('Tagging in Notebooks @addInit', () => { }); test('Can search for tags', async ({ page }) => { await createNotebookEntryAndTags(page); - // Click [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - // Fill [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); - await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); + await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); - // Click [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - // Fill [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); - await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); + await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); - // Click [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - // Fill [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); await expect(page.locator('[aria-label="Search Result"]')).toBeHidden(); - await expect(page.locator('[aria-label="Search Result"]')).toBeHidden(); }); test('Can delete tags', async ({ page }) => { await createNotebookEntryAndTags(page); await page.locator('[aria-label="Notebook Entries"]').click(); // Delete Driving - await page.hover('.c-tag__label:has-text("Driving")'); - await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click(); + await page.hover('[aria-label="Tag"]:has-text("Driving")'); + await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click(); await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving"); - // Fill [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); }); @@ -153,7 +141,6 @@ test.describe('Tagging in Notebooks @addInit', () => { await page.locator('button:has-text("OK")').click(); await page.goto('./', { waitUntil: 'networkidle' }); - // Fill [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed'); await expect(page.locator('text=No results found')).toBeVisible(); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci'); diff --git a/src/api/annotation/AnnotationAPI.js b/src/api/annotation/AnnotationAPI.js index 6b9e910be0..148b1d3c4b 100644 --- a/src/api/annotation/AnnotationAPI.js +++ b/src/api/annotation/AnnotationAPI.js @@ -22,6 +22,7 @@ import { v4 as uuid } from 'uuid'; import EventEmitter from 'EventEmitter'; +import _ from 'lodash'; /** * @readonly @@ -42,19 +43,28 @@ const ANNOTATION_TYPES = Object.freeze({ const ANNOTATION_TYPE = 'annotation'; +const ANNOTATION_LAST_CREATED = 'annotationLastCreated'; + /** * @typedef {Object} Tag * @property {String} key a unique identifier for the tag * @property {String} backgroundColor eg. "#cc0000" * @property {String} foregroundColor eg. "#ffffff" */ + export default class AnnotationAPI extends EventEmitter { + + /** + * @param {OpenMCT} openmct + */ constructor(openmct) { super(); this.openmct = openmct; this.availableTags = {}; this.ANNOTATION_TYPES = ANNOTATION_TYPES; + this.ANNOTATION_TYPE = ANNOTATION_TYPE; + this.ANNOTATION_LAST_CREATED = ANNOTATION_LAST_CREATED; this.openmct.types.addType(ANNOTATION_TYPE, { name: 'Annotation', @@ -63,6 +73,7 @@ export default class AnnotationAPI extends EventEmitter { cssClass: 'icon-notebook', initialize: function (domainObject) { domainObject.targets = domainObject.targets || {}; + domainObject._deleted = domainObject._deleted || false; domainObject.originalContextPath = domainObject.originalContextPath || ''; domainObject.tags = domainObject.tags || []; domainObject.contentText = domainObject.contentText || ''; @@ -112,6 +123,7 @@ export default class AnnotationAPI extends EventEmitter { namespace }, tags, + _deleted: false, annotationType, contentText, originalContextPath @@ -127,6 +139,7 @@ export default class AnnotationAPI extends EventEmitter { const success = await this.openmct.objects.save(createdObject); if (success) { this.emit('annotationCreated', createdObject); + this.#updateAnnotationModified(domainObject); return createdObject; } else { @@ -134,14 +147,32 @@ export default class AnnotationAPI extends EventEmitter { } } + #updateAnnotationModified(domainObject) { + this.openmct.objects.mutate(domainObject, this.ANNOTATION_LAST_CREATED, Date.now()); + } + + /** + * @method defineTag + * @param {String} key a unique identifier for the tag + * @param {Tag} tagsDefinition the definition of the tag to add + */ defineTag(tagKey, tagsDefinition) { this.availableTags[tagKey] = tagsDefinition; } + /** + * @method isAnnotation + * @param {import('../objects/ObjectAPI').DomainObject} domainObject domainObject the domainObject in question + * @returns {Boolean} Returns true if the domain object is an annotation + */ isAnnotation(domainObject) { return domainObject && (domainObject.type === ANNOTATION_TYPE); } + /** + * @method getAvailableTags + * @returns {Tag[]} Returns an array of the available tags that have been loaded + */ getAvailableTags() { if (this.availableTags) { const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => { @@ -157,18 +188,26 @@ export default class AnnotationAPI extends EventEmitter { } } - async getAnnotation(query, searchType) { - let foundAnnotation = null; + /** + * @method getAnnotations + * @param {String} query - The keystring of the domain object to search for annotations for + * @returns {import('../objects/ObjectAPI').DomainObject[]} Returns an array of domain objects that match the search query + */ + async getAnnotations(query) { + const searchResults = (await Promise.all(this.openmct.objects.search(query, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat(); - const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat(); - if (searchResults) { - foundAnnotation = searchResults[0]; - } - - return foundAnnotation; + return searchResults; } - async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) { + /** + * @method addSingleAnnotationTag + * @param {import('../objects/ObjectAPI').DomainObject=} existingAnnotation - An optional annotation to add the tag to. If not specified, we will create an annotation. + * @param {import('../objects/ObjectAPI').DomainObject} targetDomainObject - The domain object the annotation will point to. + * @param {Object=} targetSpecificDetails - Optional object to add to the target object. E.g., for notebooks this would be an entryID + * @param {AnnotationType} annotationType - The type of annotation this is for. + * @returns {import('../objects/ObjectAPI').DomainObject[]} Returns the annotation that was either created or passed as an existingAnnotation + */ + async addSingleAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) { if (!existingAnnotation) { const targets = {}; const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier); @@ -186,27 +225,44 @@ export default class AnnotationAPI extends EventEmitter { return newAnnotation; } else { - const tagArray = [tag, ...existingAnnotation.tags]; - this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray); + if (!existingAnnotation.tags.includes(tag)) { + throw new Error(`Existing annotation did not contain tag ${tag}`); + } + + if (existingAnnotation._deleted) { + this.unDeleteAnnotation(existingAnnotation); + } return existingAnnotation; } } - removeAnnotationTag(existingAnnotation, tagToRemove) { - if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) { - const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove); - this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray); - } else { - throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation); + /** + * @method deleteAnnotations + * @param {import('../objects/ObjectAPI').DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true) + */ + deleteAnnotations(annotations) { + if (!annotations) { + throw new Error('Asked to delete null annotations! 🙅‍♂️'); } + + annotations.forEach(annotation => { + if (!annotation._deleted) { + this.openmct.objects.mutate(annotation, '_deleted', true); + } + }); } - removeAnnotationTags(existingAnnotation) { - // just removes tags on the annotation as we can't really delete objects - if (existingAnnotation && existingAnnotation.tags) { - this.openmct.objects.mutate(existingAnnotation, 'tags', []); + /** + * @method deleteAnnotations + * @param {import('../objects/ObjectAPI').DomainObject} existingAnnotation - An annotations to undelete (set _deleted to false) + */ + unDeleteAnnotation(annotation) { + if (!annotation) { + throw new Error('Asked to undelete null annotation! 🙅‍♂️'); } + + this.openmct.objects.mutate(annotation, '_deleted', false); } #getMatchingTags(query) { @@ -266,16 +322,36 @@ export default class AnnotationAPI extends EventEmitter { return modelAddedToResults; } + #combineSameTargets(results) { + const combinedResults = []; + results.forEach(currentAnnotation => { + const existingAnnotation = combinedResults.find((annotationToFind) => { + return _.isEqual(currentAnnotation.targets, annotationToFind.targets); + }); + if (!existingAnnotation) { + combinedResults.push(currentAnnotation); + } else { + existingAnnotation.tags.push(...currentAnnotation.tags); + } + }); + + return combinedResults; + } + /** * @method searchForTags * @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving" - * @param {Object} abortController An optional abort method to stop the query + * @param {Object} [abortController] An optional abort method to stop the query * @returns {Promise} returns a model of matching tags with their target domain objects attached */ async searchForTags(query, abortController) { const matchingTagKeys = this.#getMatchingTags(query); const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat(); - const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys); + const filteredDeletedResults = searchResults.filter((result) => { + return !(result._deleted); + }); + const combinedSameTargets = this.#combineSameTargets(filteredDeletedResults); + const appliedTagSearchResults = this.#addTagMetaInformationToResults(combinedSameTargets, matchingTagKeys); const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults); const resultsWithValidPath = appliedTargetsModels.filter(result => { return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath); diff --git a/src/api/annotation/AnnotationAPISpec.js b/src/api/annotation/AnnotationAPISpec.js index 8aa45864d8..a7e2a162dd 100644 --- a/src/api/annotation/AnnotationAPISpec.js +++ b/src/api/annotation/AnnotationAPISpec.js @@ -126,34 +126,44 @@ describe("The Annotation API", () => { describe("Tagging", () => { it("can create a tag", async () => { - const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); + const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); expect(annotationObject).toBeDefined(); expect(annotationObject.type).toEqual('annotation'); expect(annotationObject.tags).toContain('aWonderfulTag'); }); it("can delete a tag", async () => { - const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); - const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove'); + const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); expect(annotationObject).toBeDefined(); - openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove'); - expect(annotationObject.tags).toEqual(['aWonderfulTag']); - openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag'); - expect(annotationObject.tags).toEqual([]); + openmct.annotation.deleteAnnotations([annotationObject]); + expect(annotationObject._deleted).toBeTrue(); }); it("throws an error if deleting non-existent tag", async () => { - const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); + const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); expect(annotationObject).toBeDefined(); expect(() => { openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist'); }).toThrow(); }); it("can remove all tags", async () => { - const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); + const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); expect(annotationObject).toBeDefined(); expect(() => { - openmct.annotation.removeAnnotationTags(annotationObject); + openmct.annotation.deleteAnnotations([annotationObject]); }).not.toThrow(); - expect(annotationObject.tags).toEqual([]); + expect(annotationObject._deleted).toBeTrue(); + }); + it("can add/delete/add a tag", async () => { + let annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); + expect(annotationObject).toBeDefined(); + expect(annotationObject.type).toEqual('annotation'); + expect(annotationObject.tags).toContain('aWonderfulTag'); + openmct.annotation.deleteAnnotations([annotationObject]); + expect(annotationObject._deleted).toBeTrue(); + annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); + expect(annotationObject).toBeDefined(); + expect(annotationObject.type).toEqual('annotation'); + expect(annotationObject.tags).toContain('aWonderfulTag'); + expect(annotationObject._deleted).toBeFalse(); }); }); @@ -175,16 +185,5 @@ describe("The Annotation API", () => { expect(results).toBeDefined(); expect(results.length).toEqual(1); }); - it("can get notebook annotations", async () => { - const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier); - const query = { - targetKeyString, - entryId: 'fooBarEntry' - }; - - const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS); - expect(results).toBeDefined(); - expect(results.tags.length).toEqual(2); - }); }); }); diff --git a/src/api/composition/CompositionCollection.js b/src/api/composition/CompositionCollection.js index e04bbe888a..78a0a0c8a1 100644 --- a/src/api/composition/CompositionCollection.js +++ b/src/api/composition/CompositionCollection.js @@ -199,7 +199,7 @@ define([ if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) { let keyString = this.publicAPI.objects.makeKeyString(child.identifier); - child = this.publicAPI.objects._toMutable(child); + child = this.publicAPI.objects.toMutable(child); this.mutables[keyString] = child; } diff --git a/src/api/objects/InMemorySearchProvider.js b/src/api/objects/InMemorySearchProvider.js index 6feadaf444..2510b248b8 100644 --- a/src/api/objects/InMemorySearchProvider.js +++ b/src/api/objects/InMemorySearchProvider.js @@ -42,7 +42,6 @@ class InMemorySearchProvider { this.openmct = openmct; this.indexedIds = {}; this.indexedCompositions = {}; - this.indexedTags = {}; this.idsToIndex = []; this.pendingIndex = {}; this.pendingRequests = 0; @@ -61,7 +60,6 @@ class InMemorySearchProvider { this.localSearchForObjects = this.localSearchForObjects.bind(this); this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this); this.localSearchForTags = this.localSearchForTags.bind(this); - this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this); this.onAnnotationCreation = this.onAnnotationCreation.bind(this); this.onCompositionAdded = this.onCompositionAdded.bind(this); this.onCompositionRemoved = this.onCompositionRemoved.bind(this); @@ -93,7 +91,7 @@ class InMemorySearchProvider { this.searchTypes = this.openmct.objects.SEARCH_TYPES; - this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS]; + this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.TAGS]; this.scheduleForIndexing(rootObject.identifier); @@ -163,8 +161,6 @@ class InMemorySearchProvider { return this.localSearchForObjects(queryId, query, maxResults); } else if (searchType === this.searchTypes.ANNOTATIONS) { return this.localSearchForAnnotations(queryId, query, maxResults); - } else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) { - return this.localSearchForNotebookAnnotations(queryId, query, maxResults); } else if (searchType === this.searchTypes.TAGS) { return this.localSearchForTags(queryId, query, maxResults); } else { @@ -281,13 +277,6 @@ class InMemorySearchProvider { provider.index(domainObject); } - onTagMutation(domainObject, newTags) { - domainObject.tags = newTags; - const provider = this; - - provider.index(domainObject); - } - onCompositionAdded(newDomainObjectToIndex) { const provider = this; // The object comes in as a mutable domain object, which has functions, @@ -342,14 +331,6 @@ class InMemorySearchProvider { composition.on('remove', this.onCompositionRemoved); this.indexedCompositions[keyString] = composition; } - - if (domainObject.type === 'annotation') { - this.indexedTags[keyString] = this.openmct.objects.observe( - domainObject, - 'tags', - this.onTagMutation.bind(this, domainObject) - ); - } } if ((keyString !== 'ROOT')) { @@ -581,43 +562,6 @@ class InMemorySearchProvider { this.onWorkerMessage(eventToReturn); } - /** - * A local version of the same SharedWorker function - * if we don't have SharedWorkers available (e.g., iOS) - */ - localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) { - // This results dictionary will have domain object ID keys which - // point to the value the domain object's score. - let results = []; - const message = { - request: 'searchForNotebookAnnotations', - results: [], - total: 0, - queryId - }; - - const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString]; - if (matchingAnnotations) { - results = matchingAnnotations.filter(matchingAnnotation => { - if (!matchingAnnotation.targets) { - return false; - } - - const target = matchingAnnotation.targets[targetKeyString]; - - return (target && target.entryId && (target.entryId === entryId)); - }); - } - - message.total = results.length; - message.results = results - .slice(0, maxResults); - const eventToReturn = { - data: message - }; - this.onWorkerMessage(eventToReturn); - } - destroyObservers(observers) { Object.entries(observers).forEach(([keyString, unobserve]) => { if (typeof unobserve === 'function') { diff --git a/src/api/objects/InMemorySearchWorker.js b/src/api/objects/InMemorySearchWorker.js index a2bb53a026..71d8fe86b0 100644 --- a/src/api/objects/InMemorySearchWorker.js +++ b/src/api/objects/InMemorySearchWorker.js @@ -43,8 +43,6 @@ port.postMessage(searchForAnnotations(event.data)); } else if (requestType === 'TAGS') { port.postMessage(searchForTags(event.data)); - } else if (requestType === 'NOTEBOOK_ANNOTATIONS') { - port.postMessage(searchForNotebookAnnotations(event.data)); } else { throw new Error(`Unknown request ${event.data.request}`); } @@ -204,33 +202,4 @@ return message; } - - function searchForNotebookAnnotations(data) { - let results = []; - const message = { - request: 'searchForNotebookAnnotations', - results: {}, - total: 0, - queryId: data.queryId - }; - - const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString]; - if (matchingAnnotations) { - results = matchingAnnotations.filter(matchingAnnotation => { - if (!matchingAnnotation.targets) { - return false; - } - - const target = matchingAnnotation.targets[data.input.targetKeyString]; - - return (target && target.entryId && (target.entryId === data.input.entryId)); - }); - } - - message.total = results.length; - message.results = results - .slice(0, data.maxResults); - - return message; - } }()); diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index 64167f3c75..1d0ccfe25c 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -64,6 +64,15 @@ import InMemorySearchProvider from './InMemorySearchProvider'; * to load domain objects * @memberof module:openmct */ + +/** + * @readonly + * @enum {String} SEARCH_TYPES + * @property {String} OBJECTS Search for objects + * @property {String} ANNOTATIONS Search for annotations + * @property {String} TAGS Search for tags +*/ + /** * Utilities for loading, saving, and manipulating domain objects. * @interface ObjectAPI @@ -76,7 +85,6 @@ export default class ObjectAPI { this.SEARCH_TYPES = Object.freeze({ OBJECTS: 'OBJECTS', ANNOTATIONS: 'ANNOTATIONS', - NOTEBOOK_ANNOTATIONS: 'NOTEBOOK_ANNOTATIONS', TAGS: 'TAGS' }); this.eventEmitter = new EventEmitter(); @@ -188,7 +196,6 @@ export default class ObjectAPI { * @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) { let keystring = this.makeKeyString(identifier); @@ -223,7 +230,7 @@ export default class ObjectAPI { if (result.isMutable) { result.$refresh(result); } else { - let mutableDomainObject = this._toMutable(result); + let mutableDomainObject = this.toMutable(result); mutableDomainObject.$refresh(result); } @@ -300,7 +307,7 @@ export default class ObjectAPI { } return this.get(identifier).then((object) => { - return this._toMutable(object); + return this.toMutable(object); }); } @@ -490,7 +497,7 @@ export default class ObjectAPI { } else { //Creating a temporary mutable domain object allows other mutable instances of the //object to be kept in sync. - let mutableDomainObject = this._toMutable(domainObject); + let mutableDomainObject = this.toMutable(domainObject); //Mutate original object MutableDomainObject.mutateObject(domainObject, path, value); @@ -510,15 +517,19 @@ export default class ObjectAPI { } /** - * @private + * Create a mutable domain object from an existing domain object + * @param {module:openmct.DomainObject} domainObject the object to make mutable + * @returns {MutableDomainObject} a mutable domain object that will automatically sync + * @method toMutable + * @memberof module:openmct.ObjectAPI# */ - _toMutable(object) { + toMutable(domainObject) { let mutableObject; - if (object.isMutable) { - mutableObject = object; + if (domainObject.isMutable) { + mutableObject = domainObject; } else { - mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter); + mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter); // Check if provider supports realtime updates let identifier = utils.parseKeyString(mutableObject.identifier); @@ -526,9 +537,11 @@ export default class ObjectAPI { if (provider !== undefined && provider.observe !== undefined - && this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) { + && this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) { let unobserve = provider.observe(identifier, (updatedModel) => { - if (updatedModel.persisted > mutableObject.modified) { + // modified can sometimes be undefined, so make it 0 in this case + const mutableObjectModification = mutableObject.modified ?? Number.MIN_SAFE_INTEGER; + if (updatedModel.persisted > mutableObjectModification) { //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. updatedModel = this.applyGetInterceptors(identifier, updatedModel); @@ -582,7 +595,7 @@ export default class ObjectAPI { if (domainObject.isMutable) { return domainObject.$observe(path, callback); } else { - let mutable = this._toMutable(domainObject); + let mutable = this.toMutable(domainObject); mutable.$observe(path, callback); return () => mutable.$destroy(); @@ -675,8 +688,10 @@ export default class ObjectAPI { } #hasAlreadyBeenPersisted(domainObject) { + // modified can sometimes be undefined, so make it 0 in this case + const modified = domainObject.modified ?? Number.MIN_SAFE_INTEGER; const result = domainObject.persisted !== undefined - && domainObject.persisted >= domainObject.modified; + && domainObject.persisted >= modified; return result; } diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js index e473fc572e..950b356be0 100644 --- a/src/api/objects/ObjectAPISpec.js +++ b/src/api/objects/ObjectAPISpec.js @@ -320,7 +320,7 @@ describe("The Object API", () => { beforeEach(function () { // Duplicate object to guarantee we are not sharing object instance, which would invalidate test testObjectDuplicate = JSON.parse(JSON.stringify(testObject)); - mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate); + mutableSecondInstance = objectAPI.toMutable(testObjectDuplicate); }); afterEach(() => { diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index 9719de33e9..327205a925 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -151,6 +151,7 @@ :key="entry.id" :entry="entry" :domain-object="domainObject" + :notebook-annotations="notebookAnnotations[entry.id]" :selected-page="selectedPage" :selected-section="selectedSection" :read-only="false" @@ -219,10 +220,12 @@ export default { isRestricted: this.domainObject.type === RESTRICTED_NOTEBOOK_TYPE, search: '', searchResults: [], + lastLocalAnnotationCreation: 0, showTime: this.domainObject.configuration.showTime || 0, showNav: false, sidebarCoversEntries: false, - filteredAndSortedEntries: [] + filteredAndSortedEntries: [], + notebookAnnotations: {} }; }, computed: { @@ -289,7 +292,8 @@ export default { this.getSearchResults = debounce(this.getSearchResults, 500); this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100); }, - mounted() { + async mounted() { + await this.loadAnnotations(); this.formatSidebar(); this.setSectionAndPageFromUrl(); @@ -307,6 +311,13 @@ export default { this.unobserveEntries(); } + Object.keys(this.notebookAnnotations).forEach(entryID => { + const notebookAnnotationsForEntry = this.notebookAnnotations[entryID]; + notebookAnnotationsForEntry.forEach(notebookAnnotation => { + this.openmct.objects.destroyMutable(notebookAnnotation); + }); + }); + window.removeEventListener('orientationchange', this.formatSidebar); window.removeEventListener('hashchange', this.setSectionAndPageFromUrl); }, @@ -338,6 +349,32 @@ export default { } }); }, + async loadAnnotations() { + if (!this.openmct.annotation.getAvailableTags().length) { + return; + } + + this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0; + + const query = this.openmct.objects.makeKeyString(this.domainObject.identifier); + const foundAnnotations = await this.openmct.annotation.getAnnotations(query); + foundAnnotations.forEach((foundAnnotation) => { + const targetId = Object.keys(foundAnnotation.targets)[0]; + const entryId = foundAnnotation.targets[targetId].entryId; + if (!this.notebookAnnotations[entryId]) { + this.$set(this.notebookAnnotations, entryId, []); + } + + const annotationExtant = this.notebookAnnotations[entryId].some((existingAnnotation) => { + return this.openmct.objects.areIdsEqual(existingAnnotation.identifier, foundAnnotation.identifier); + }); + if (!annotationExtant) { + const annotationArray = this.notebookAnnotations[entryId]; + const mutableAnnotation = this.openmct.objects.toMutable(foundAnnotation); + annotationArray.push(mutableAnnotation); + } + }); + }, filterAndSortEntries() { const filterTime = Date.now(); const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || []; @@ -350,6 +387,10 @@ export default { this.filteredAndSortedEntries = this.defaultSort === 'oldest' ? filteredPageEntriesByTime : [...filteredPageEntriesByTime].reverse(); + + if (this.lastLocalAnnotationCreation < this.domainObject.annotationLastCreated) { + this.loadAnnotations(); + } }, changeSelectedSection({ sectionId, pageId }) { const sections = this.sections.map(s => { @@ -473,14 +514,8 @@ export default { ] }); }, - async removeAnnotations(entryId) { - const targetKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); - const query = { - targetKeyString, - entryId - }; - const existingAnnotation = await this.openmct.annotation.getAnnotation(query, this.openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS); - this.openmct.annotation.removeAnnotationTags(existingAnnotation); + removeAnnotations(entryId) { + this.openmct.annotation.deleteAnnotations(this.notebookAnnotations[entryId]); }, checkEntryPos(entry) { const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage); diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 947d1b4ffd..58677621d3 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -84,9 +84,8 @@ @@ -163,6 +162,12 @@ export default { return {}; } }, + notebookAnnotations: { + type: Array, + default() { + return []; + } + }, entry: { type: Object, default() { @@ -204,15 +209,6 @@ export default { createdOnDate() { return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD'); }, - annotationQuery() { - const targetKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); - - return { - targetKeyString, - entryId: this.entry.id, - modified: this.entry.modified - }; - }, createdOnTime() { return this.formatTime(this.entry.createdOn, 'HH:mm:ss'); }, diff --git a/src/plugins/notebook/components/Sidebar.vue b/src/plugins/notebook/components/Sidebar.vue index c7476d035c..5e1022231c 100644 --- a/src/plugins/notebook/components/Sidebar.vue +++ b/src/plugins/notebook/components/Sidebar.vue @@ -6,6 +6,7 @@ {{ sectionTitle }}