mirror of
https://github.com/nasa/openmct.git
synced 2024-12-19 21:27:52 +00:00
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 <ozyx@users.noreply.github.com> * Update src/api/objects/ObjectAPI.js Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> * Update src/plugins/notebook/components/Notebook.vue Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> * 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 <john.c.hill@nasa.gov> Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
This commit is contained in:
parent
27c30132d2
commit
ce463babff
@ -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
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
@ -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');
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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') {
|
||||
|
@ -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;
|
||||
}
|
||||
}());
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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(() => {
|
||||
|
@ -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);
|
||||
|
@ -84,9 +84,8 @@
|
||||
|
||||
<TagEditor
|
||||
:domain-object="domainObject"
|
||||
:annotation-query="annotationQuery"
|
||||
:annotations="notebookAnnotations"
|
||||
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
||||
:annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
|
||||
:target-specific-details="{entryId: entry.id}"
|
||||
@tags-updated="timestampAndUpdate"
|
||||
/>
|
||||
@ -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');
|
||||
},
|
||||
|
@ -6,6 +6,7 @@
|
||||
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
|
||||
<button
|
||||
class="c-icon-button c-icon-button--major icon-plus"
|
||||
aria-label="Add Section"
|
||||
@click="addSection"
|
||||
>
|
||||
<span class="c-list-button__label">Add</span>
|
||||
@ -33,6 +34,7 @@
|
||||
|
||||
<button
|
||||
class="c-icon-button c-icon-button--major icon-plus"
|
||||
aria-label="Add Page"
|
||||
@click="addPage"
|
||||
>
|
||||
<span class="c-icon-button__label">Add</span>
|
||||
|
@ -1,22 +1,23 @@
|
||||
import { isNotebookType } from './notebook-constants';
|
||||
import { isAnnotationType, isNotebookType, isNotebookOrAnnotationType } from './notebook-constants';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function (openmct) {
|
||||
const apiSave = openmct.objects.save.bind(openmct.objects);
|
||||
|
||||
openmct.objects.save = async (domainObject) => {
|
||||
if (!isNotebookType(domainObject)) {
|
||||
if (!isNotebookOrAnnotationType(domainObject)) {
|
||||
return apiSave(domainObject);
|
||||
}
|
||||
|
||||
const isNewMutable = !domainObject.isMutable;
|
||||
const localMutable = openmct.objects._toMutable(domainObject);
|
||||
const localMutable = openmct.objects.toMutable(domainObject);
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await apiSave(localMutable);
|
||||
} catch (error) {
|
||||
if (error instanceof openmct.objects.errors.Conflict) {
|
||||
result = resolveConflicts(localMutable, openmct);
|
||||
result = await resolveConflicts(domainObject, localMutable, openmct);
|
||||
} else {
|
||||
result = Promise.reject(error);
|
||||
}
|
||||
@ -30,16 +31,56 @@ export default function (openmct) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveConflicts(localMutable, openmct) {
|
||||
const localEntries = JSON.parse(JSON.stringify(localMutable.configuration.entries));
|
||||
function resolveConflicts(domainObject, localMutable, openmct) {
|
||||
if (isNotebookType(domainObject)) {
|
||||
return resolveNotebookEntryConflicts(localMutable, openmct);
|
||||
} else if (isAnnotationType(domainObject)) {
|
||||
return resolveNotebookTagConflicts(localMutable, openmct);
|
||||
}
|
||||
}
|
||||
|
||||
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
|
||||
applyLocalEntries(remoteMutable, localEntries, openmct);
|
||||
async function resolveNotebookTagConflicts(localAnnotation, openmct) {
|
||||
const localClonedAnnotation = structuredClone(localAnnotation);
|
||||
const remoteMutable = await openmct.objects.getMutable(localClonedAnnotation.identifier);
|
||||
|
||||
openmct.objects.destroyMutable(remoteMutable);
|
||||
// should only be one annotation per targetID, entryID, and tag; so for sanity, ensure we have the
|
||||
// same targetID, entryID, and tags for this conflict
|
||||
if (!(_.isEqual(remoteMutable.tags, localClonedAnnotation.tags))) {
|
||||
throw new Error('Conflict on annotation\'s tag has different tags than remote');
|
||||
}
|
||||
|
||||
return true;
|
||||
Object.keys(localClonedAnnotation.targets).forEach(targetKey => {
|
||||
if (!remoteMutable.targets[targetKey]) {
|
||||
throw new Error(`Conflict on annotation's target is missing ${targetKey}`);
|
||||
}
|
||||
|
||||
const remoteMutableTarget = remoteMutable.targets[targetKey];
|
||||
const localMutableTarget = localClonedAnnotation.targets[targetKey];
|
||||
|
||||
if (remoteMutableTarget.entryId !== localMutableTarget.entryId) {
|
||||
throw new Error(`Conflict on annotation's entryID ${remoteMutableTarget.entryId} has a different entry Id ${localMutableTarget.entryId}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (remoteMutable._deleted && (remoteMutable._deleted !== localClonedAnnotation._deleted)) {
|
||||
// not deleting wins 😘
|
||||
openmct.objects.mutate(remoteMutable, '_deleted', false);
|
||||
}
|
||||
|
||||
openmct.objects.destroyMutable(remoteMutable);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function resolveNotebookEntryConflicts(localMutable, openmct) {
|
||||
if (localMutable.configuration.entries) {
|
||||
const localEntries = structuredClone(localMutable.configuration.entries);
|
||||
const remoteMutable = await openmct.objects.getMutable(localMutable.identifier);
|
||||
applyLocalEntries(remoteMutable, localEntries, openmct);
|
||||
openmct.objects.destroyMutable(remoteMutable);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyLocalEntries(mutable, entries, openmct) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
export const NOTEBOOK_TYPE = 'notebook';
|
||||
export const RESTRICTED_NOTEBOOK_TYPE = 'restricted-notebook';
|
||||
export const ANNOTATION_TYPE = 'annotation';
|
||||
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
|
||||
export const NOTEBOOK_DEFAULT = 'DEFAULT';
|
||||
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
|
||||
@ -9,10 +10,18 @@ export const NOTEBOOK_INSTALLED_KEY = '_NOTEBOOK_PLUGIN_INSTALLED';
|
||||
export const RESTRICTED_NOTEBOOK_INSTALLED_KEY = '_RESTRICTED_NOTEBOOK_PLUGIN_INSTALLED';
|
||||
|
||||
// these only deals with constants, figured this could skip going into a utils file
|
||||
export function isNotebookOrAnnotationType(domainObject) {
|
||||
return (isNotebookType(domainObject) || isAnnotationType(domainObject));
|
||||
}
|
||||
|
||||
export function isNotebookType(domainObject) {
|
||||
return [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE].includes(domainObject.type);
|
||||
}
|
||||
|
||||
export function isAnnotationType(domainObject) {
|
||||
return [ANNOTATION_TYPE].includes(domainObject.type);
|
||||
}
|
||||
|
||||
export function isNotebookViewType(view) {
|
||||
return [NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_VIEW_TYPE].includes(view);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export default class AbstractStatusIndicator {
|
||||
#configuration;
|
||||
|
||||
/**
|
||||
* @param {*} openmct the Open MCT API (proper jsdoc to come)
|
||||
* @param {*} openmct the Open MCT API (proper typescript doc to come)
|
||||
* @param {import('@/api/user/UserAPI').UserAPIConfiguration} configuration Per-deployment status styling. See the type definition in UserAPI
|
||||
*/
|
||||
constructor(openmct, configuration) {
|
||||
|
@ -23,7 +23,7 @@
|
||||
import CouchDocument from "./CouchDocument";
|
||||
import CouchObjectQueue from "./CouchObjectQueue";
|
||||
import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator";
|
||||
import { isNotebookType } from '../../notebook/notebook-constants.js';
|
||||
import { isNotebookOrAnnotationType } from '../../notebook/notebook-constants.js';
|
||||
|
||||
const REV = "_rev";
|
||||
const ID = "_id";
|
||||
@ -71,7 +71,7 @@ class CouchObjectProvider {
|
||||
}
|
||||
|
||||
onSharedWorkerMessageError(event) {
|
||||
console.log('Error', event);
|
||||
console.error('Error', event);
|
||||
}
|
||||
|
||||
isSynchronizedObject(object) {
|
||||
@ -290,7 +290,7 @@ class CouchObjectProvider {
|
||||
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
|
||||
}
|
||||
|
||||
if (isNotebookType(object) || object.type === 'annotation') {
|
||||
if (isNotebookOrAnnotationType(object)) {
|
||||
//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]);
|
||||
|
@ -31,7 +31,7 @@ class CouchSearchProvider {
|
||||
constructor(couchObjectProvider) {
|
||||
this.couchObjectProvider = couchObjectProvider;
|
||||
this.searchTypes = couchObjectProvider.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];
|
||||
}
|
||||
|
||||
supportsSearchType(searchType) {
|
||||
@ -43,8 +43,6 @@ class CouchSearchProvider {
|
||||
return this.searchForObjects(query, abortSignal);
|
||||
} else if (searchType === this.searchTypes.ANNOTATIONS) {
|
||||
return this.searchForAnnotations(query, abortSignal);
|
||||
} else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
|
||||
return this.searchForNotebookAnnotations(query, abortSignal);
|
||||
} else if (searchType === this.searchTypes.TAGS) {
|
||||
return this.searchForTags(query, abortSignal);
|
||||
} else {
|
||||
@ -91,38 +89,6 @@ class CouchSearchProvider {
|
||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||
}
|
||||
|
||||
searchForNotebookAnnotations({targetKeyString, entryId}, abortSignal) {
|
||||
const filter = {
|
||||
"selector": {
|
||||
"$and": [
|
||||
{
|
||||
"model.type": {
|
||||
"$eq": "annotation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model.annotationType": {
|
||||
"$eq": "NOTEBOOK"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": {
|
||||
"targets": {
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
filter.selector.$and[2].model.targets[targetKeyString] = {
|
||||
"entryId": {
|
||||
"$eq": entryId
|
||||
}
|
||||
};
|
||||
|
||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||
}
|
||||
|
||||
searchForTags(tagsArray, abortSignal) {
|
||||
const filter = {
|
||||
"selector": {
|
||||
@ -130,7 +96,8 @@ class CouchSearchProvider {
|
||||
{
|
||||
"model.tags": {
|
||||
"$elemMatch": {
|
||||
"$eq": `${tagsArray[0]}`
|
||||
"$or": [
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -142,6 +109,11 @@ class CouchSearchProvider {
|
||||
]
|
||||
}
|
||||
};
|
||||
tagsArray.forEach(tag => {
|
||||
filter.selector.$and[0]["model.tags"].$elemMatch.$or.push({
|
||||
"$eq": `${tag}`
|
||||
});
|
||||
});
|
||||
|
||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||
}
|
||||
|
@ -244,7 +244,7 @@ define(
|
||||
|
||||
if (context.item && context.item.isMutable !== true) {
|
||||
removeMutable = true;
|
||||
context.item = this.openmct.objects._toMutable(context.item);
|
||||
context.item = this.openmct.objects.toMutable(context.item);
|
||||
}
|
||||
|
||||
if (select) {
|
||||
|
@ -51,18 +51,14 @@ export default {
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
annotationQuery: {
|
||||
type: Object,
|
||||
annotations: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
annotationType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
annotationSearchType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
targetSpecificDetails: {
|
||||
type: Object,
|
||||
required: true
|
||||
@ -76,7 +72,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
annontation: null,
|
||||
addedTags: [],
|
||||
userAddingTag: false
|
||||
};
|
||||
@ -92,57 +87,50 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
annotation: {
|
||||
annotations: {
|
||||
handler() {
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
annotationQuery: {
|
||||
handler() {
|
||||
this.unloadAnnotation();
|
||||
this.loadAnnotation();
|
||||
this.annotationsChanged();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadAnnotation();
|
||||
},
|
||||
destroyed() {
|
||||
if (this.removeTagsListener) {
|
||||
this.removeTagsListener();
|
||||
}
|
||||
this.annotationsChanged();
|
||||
},
|
||||
methods: {
|
||||
addAnnotationListener(annotation) {
|
||||
if (annotation && !this.removeTagsListener) {
|
||||
this.removeTagsListener = this.openmct.objects.observe(annotation, '*', (newAnnotation) => {
|
||||
this.tagsChanged(newAnnotation.tags);
|
||||
this.annotation = newAnnotation;
|
||||
});
|
||||
annotationsChanged() {
|
||||
if (this.annotations && this.annotations.length) {
|
||||
this.tagsChanged();
|
||||
}
|
||||
},
|
||||
async loadAnnotation() {
|
||||
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
|
||||
this.addAnnotationListener(this.annotation);
|
||||
if (this.annotation && this.annotation.tags) {
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
annotationDeletionListener(changedAnnotation) {
|
||||
const matchingAnnotation = this.annotations.find((possibleMatchingAnnotation) => {
|
||||
return this.openmct.objects.areIdsEqual(possibleMatchingAnnotation.identifier, changedAnnotation.identifier);
|
||||
});
|
||||
if (matchingAnnotation) {
|
||||
matchingAnnotation._deleted = changedAnnotation._deleted;
|
||||
this.userAddingTag = false;
|
||||
this.tagsChanged();
|
||||
}
|
||||
},
|
||||
unloadAnnotation() {
|
||||
if (this.removeTagsListener) {
|
||||
this.removeTagsListener();
|
||||
this.removeTagsListener = undefined;
|
||||
}
|
||||
},
|
||||
tagsChanged(newTags) {
|
||||
if (newTags.length < this.addedTags.length) {
|
||||
this.addedTags = this.addedTags.slice(0, newTags.length);
|
||||
tagsChanged() {
|
||||
// gather tags from annotations
|
||||
const tagsFromAnnotations = this.annotations.flatMap((annotation) => {
|
||||
if (annotation._deleted) {
|
||||
return [];
|
||||
} else {
|
||||
return annotation.tags;
|
||||
}
|
||||
}).filter((tag, index, array) => {
|
||||
return array.indexOf(tag) === index;
|
||||
});
|
||||
|
||||
if (tagsFromAnnotations.length !== this.addedTags.length) {
|
||||
this.addedTags = this.addedTags.slice(0, tagsFromAnnotations.length);
|
||||
}
|
||||
|
||||
for (let index = 0; index < newTags.length; index += 1) {
|
||||
this.$set(this.addedTags, index, newTags[index]);
|
||||
for (let index = 0; index < tagsFromAnnotations.length; index += 1) {
|
||||
this.$set(this.addedTags, index, tagsFromAnnotations[index]);
|
||||
}
|
||||
},
|
||||
addTag() {
|
||||
@ -153,23 +141,27 @@ export default {
|
||||
this.userAddingTag = true;
|
||||
},
|
||||
async tagRemoved(tagToRemove) {
|
||||
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||
this.$emit('tags-updated');
|
||||
// Soft delete annotations that match tag instead
|
||||
const annotationsToDelete = this.annotations.filter((annotation) => {
|
||||
return annotation.tags.includes(tagToRemove);
|
||||
});
|
||||
const result = await this.openmct.annotation.deleteAnnotations(annotationsToDelete);
|
||||
this.$emit('tags-updated', annotationsToDelete);
|
||||
|
||||
return result;
|
||||
},
|
||||
async tagAdded(newTag) {
|
||||
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
|
||||
this.annotation = await this.openmct.annotation.addAnnotationTag(this.annotation,
|
||||
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
|
||||
if (annotationWasCreated) {
|
||||
this.addAnnotationListener(this.annotation);
|
||||
}
|
||||
// Either undelete an annotation, or create one (1) new annotation
|
||||
const existingAnnotation = this.annotations.find((annotation) => {
|
||||
return annotation.tags.includes(newTag);
|
||||
});
|
||||
|
||||
const createdAnnotation = await this.openmct.annotation.addSingleAnnotationTag(existingAnnotation,
|
||||
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
|
||||
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
this.userAddingTag = false;
|
||||
|
||||
this.$emit('tags-updated');
|
||||
this.$emit('tags-updated', createdAnnotation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -37,7 +37,10 @@
|
||||
class="c-tag"
|
||||
:style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
|
||||
>
|
||||
<div class="c-tag__label">{{ selectedTagLabel }} </div>
|
||||
<div
|
||||
class="c-tag__label"
|
||||
aria-label="Tag"
|
||||
>{{ selectedTagLabel }} </div>
|
||||
<button
|
||||
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
|
||||
@click="removeTag"
|
||||
|
@ -19,7 +19,7 @@ module.exports = merge(common, {
|
||||
// See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
|
||||
ignored: [
|
||||
'**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e,
|
||||
'**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json,jsdoc.json}', // Config files
|
||||
'**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json}', // Config files
|
||||
'**/*.{sh,md,png,ttf,woff,svg}', // Non source files
|
||||
'**/.*' // dotfiles and dotfolders
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user