mirror of
https://github.com/nasa/openmct.git
synced 2025-05-04 17:52:58 +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
|
## Sections
|
||||||
|
|
||||||
* The [API](api/) document is generated from inline documentation
|
* The [API](api/) uses inline documentation
|
||||||
using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and
|
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.
|
functions that make up the software platform.
|
||||||
|
|
||||||
* The [Development Process](process/) document describes the
|
* 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 }) => {
|
test('Can load tags', async ({ page }) => {
|
||||||
|
|
||||||
await createNotebookAndEntry(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();
|
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 page.locator('[placeholder="Type to select tag"]').click();
|
||||||
|
|
||||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
|
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("Science");
|
||||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
|
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();
|
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 page.locator('[placeholder="Type to select tag"]').click();
|
||||||
|
|
||||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
|
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 }) => {
|
test('Can search for tags', async ({ page }) => {
|
||||||
await createNotebookEntryAndTags(page);
|
await createNotebookEntryAndTags(page);
|
||||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
|
||||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
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 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("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();
|
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 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("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();
|
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 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();
|
||||||
await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can delete tags', async ({ page }) => {
|
test('Can delete tags', async ({ page }) => {
|
||||||
await createNotebookEntryAndTags(page);
|
await createNotebookEntryAndTags(page);
|
||||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||||
// Delete Driving
|
// Delete Driving
|
||||||
await page.hover('.c-tag__label:has-text("Driving")');
|
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||||
await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
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"]')).toContainText("Science");
|
||||||
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
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 page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
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.locator('button:has-text("OK")').click();
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
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 page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
|
||||||
await expect(page.locator('text=No results found')).toBeVisible();
|
await expect(page.locator('text=No results found')).toBeVisible();
|
||||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import EventEmitter from 'EventEmitter';
|
import EventEmitter from 'EventEmitter';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @readonly
|
* @readonly
|
||||||
@ -42,19 +43,28 @@ const ANNOTATION_TYPES = Object.freeze({
|
|||||||
|
|
||||||
const ANNOTATION_TYPE = 'annotation';
|
const ANNOTATION_TYPE = 'annotation';
|
||||||
|
|
||||||
|
const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Tag
|
* @typedef {Object} Tag
|
||||||
* @property {String} key a unique identifier for the tag
|
* @property {String} key a unique identifier for the tag
|
||||||
* @property {String} backgroundColor eg. "#cc0000"
|
* @property {String} backgroundColor eg. "#cc0000"
|
||||||
* @property {String} foregroundColor eg. "#ffffff"
|
* @property {String} foregroundColor eg. "#ffffff"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class AnnotationAPI extends EventEmitter {
|
export default class AnnotationAPI extends EventEmitter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {OpenMCT} openmct
|
||||||
|
*/
|
||||||
constructor(openmct) {
|
constructor(openmct) {
|
||||||
super();
|
super();
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.availableTags = {};
|
this.availableTags = {};
|
||||||
|
|
||||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||||
|
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
|
||||||
|
this.ANNOTATION_LAST_CREATED = ANNOTATION_LAST_CREATED;
|
||||||
|
|
||||||
this.openmct.types.addType(ANNOTATION_TYPE, {
|
this.openmct.types.addType(ANNOTATION_TYPE, {
|
||||||
name: 'Annotation',
|
name: 'Annotation',
|
||||||
@ -63,6 +73,7 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
cssClass: 'icon-notebook',
|
cssClass: 'icon-notebook',
|
||||||
initialize: function (domainObject) {
|
initialize: function (domainObject) {
|
||||||
domainObject.targets = domainObject.targets || {};
|
domainObject.targets = domainObject.targets || {};
|
||||||
|
domainObject._deleted = domainObject._deleted || false;
|
||||||
domainObject.originalContextPath = domainObject.originalContextPath || '';
|
domainObject.originalContextPath = domainObject.originalContextPath || '';
|
||||||
domainObject.tags = domainObject.tags || [];
|
domainObject.tags = domainObject.tags || [];
|
||||||
domainObject.contentText = domainObject.contentText || '';
|
domainObject.contentText = domainObject.contentText || '';
|
||||||
@ -112,6 +123,7 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
namespace
|
namespace
|
||||||
},
|
},
|
||||||
tags,
|
tags,
|
||||||
|
_deleted: false,
|
||||||
annotationType,
|
annotationType,
|
||||||
contentText,
|
contentText,
|
||||||
originalContextPath
|
originalContextPath
|
||||||
@ -127,6 +139,7 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
const success = await this.openmct.objects.save(createdObject);
|
const success = await this.openmct.objects.save(createdObject);
|
||||||
if (success) {
|
if (success) {
|
||||||
this.emit('annotationCreated', createdObject);
|
this.emit('annotationCreated', createdObject);
|
||||||
|
this.#updateAnnotationModified(domainObject);
|
||||||
|
|
||||||
return createdObject;
|
return createdObject;
|
||||||
} else {
|
} 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) {
|
defineTag(tagKey, tagsDefinition) {
|
||||||
this.availableTags[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) {
|
isAnnotation(domainObject) {
|
||||||
return domainObject && (domainObject.type === ANNOTATION_TYPE);
|
return domainObject && (domainObject.type === ANNOTATION_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method getAvailableTags
|
||||||
|
* @returns {Tag[]} Returns an array of the available tags that have been loaded
|
||||||
|
*/
|
||||||
getAvailableTags() {
|
getAvailableTags() {
|
||||||
if (this.availableTags) {
|
if (this.availableTags) {
|
||||||
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
|
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();
|
return searchResults;
|
||||||
if (searchResults) {
|
|
||||||
foundAnnotation = searchResults[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundAnnotation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (!existingAnnotation) {
|
||||||
const targets = {};
|
const targets = {};
|
||||||
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
|
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
|
||||||
@ -186,27 +225,44 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
|
|
||||||
return newAnnotation;
|
return newAnnotation;
|
||||||
} else {
|
} else {
|
||||||
const tagArray = [tag, ...existingAnnotation.tags];
|
if (!existingAnnotation.tags.includes(tag)) {
|
||||||
this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
|
throw new Error(`Existing annotation did not contain tag ${tag}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingAnnotation._deleted) {
|
||||||
|
this.unDeleteAnnotation(existingAnnotation);
|
||||||
|
}
|
||||||
|
|
||||||
return existingAnnotation;
|
return existingAnnotation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAnnotationTag(existingAnnotation, tagToRemove) {
|
/**
|
||||||
if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) {
|
* @method deleteAnnotations
|
||||||
const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove);
|
* @param {import('../objects/ObjectAPI').DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
||||||
this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray);
|
*/
|
||||||
} else {
|
deleteAnnotations(annotations) {
|
||||||
throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation);
|
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
|
* @method deleteAnnotations
|
||||||
if (existingAnnotation && existingAnnotation.tags) {
|
* @param {import('../objects/ObjectAPI').DomainObject} existingAnnotation - An annotations to undelete (set _deleted to false)
|
||||||
this.openmct.objects.mutate(existingAnnotation, 'tags', []);
|
*/
|
||||||
|
unDeleteAnnotation(annotation) {
|
||||||
|
if (!annotation) {
|
||||||
|
throw new Error('Asked to undelete null annotation! 🙅♂️');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.openmct.objects.mutate(annotation, '_deleted', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#getMatchingTags(query) {
|
#getMatchingTags(query) {
|
||||||
@ -266,16 +322,36 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
return modelAddedToResults;
|
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
|
* @method searchForTags
|
||||||
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
|
* @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
|
* @returns {Promise} returns a model of matching tags with their target domain objects attached
|
||||||
*/
|
*/
|
||||||
async searchForTags(query, abortController) {
|
async searchForTags(query, abortController) {
|
||||||
const matchingTagKeys = this.#getMatchingTags(query);
|
const matchingTagKeys = this.#getMatchingTags(query);
|
||||||
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
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 appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
|
||||||
const resultsWithValidPath = appliedTargetsModels.filter(result => {
|
const resultsWithValidPath = appliedTargetsModels.filter(result => {
|
||||||
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
|
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
|
||||||
|
@ -126,34 +126,44 @@ describe("The Annotation API", () => {
|
|||||||
|
|
||||||
describe("Tagging", () => {
|
describe("Tagging", () => {
|
||||||
it("can create a tag", async () => {
|
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).toBeDefined();
|
||||||
expect(annotationObject.type).toEqual('annotation');
|
expect(annotationObject.type).toEqual('annotation');
|
||||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||||
});
|
});
|
||||||
it("can delete a tag", async () => {
|
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.addSingleAnnotationTag(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');
|
|
||||||
expect(annotationObject).toBeDefined();
|
expect(annotationObject).toBeDefined();
|
||||||
openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove');
|
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||||
expect(annotationObject.tags).toEqual(['aWonderfulTag']);
|
expect(annotationObject._deleted).toBeTrue();
|
||||||
openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag');
|
|
||||||
expect(annotationObject.tags).toEqual([]);
|
|
||||||
});
|
});
|
||||||
it("throws an error if deleting non-existent tag", async () => {
|
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(annotationObject).toBeDefined();
|
||||||
expect(() => {
|
expect(() => {
|
||||||
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
it("can remove all tags", async () => {
|
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(annotationObject).toBeDefined();
|
||||||
expect(() => {
|
expect(() => {
|
||||||
openmct.annotation.removeAnnotationTags(annotationObject);
|
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||||
}).not.toThrow();
|
}).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).toBeDefined();
|
||||||
expect(results.length).toEqual(1);
|
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)) {
|
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) {
|
||||||
let keyString = this.publicAPI.objects.makeKeyString(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;
|
this.mutables[keyString] = child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ class InMemorySearchProvider {
|
|||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.indexedIds = {};
|
this.indexedIds = {};
|
||||||
this.indexedCompositions = {};
|
this.indexedCompositions = {};
|
||||||
this.indexedTags = {};
|
|
||||||
this.idsToIndex = [];
|
this.idsToIndex = [];
|
||||||
this.pendingIndex = {};
|
this.pendingIndex = {};
|
||||||
this.pendingRequests = 0;
|
this.pendingRequests = 0;
|
||||||
@ -61,7 +60,6 @@ class InMemorySearchProvider {
|
|||||||
this.localSearchForObjects = this.localSearchForObjects.bind(this);
|
this.localSearchForObjects = this.localSearchForObjects.bind(this);
|
||||||
this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
|
this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
|
||||||
this.localSearchForTags = this.localSearchForTags.bind(this);
|
this.localSearchForTags = this.localSearchForTags.bind(this);
|
||||||
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
|
|
||||||
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
|
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
|
||||||
this.onCompositionAdded = this.onCompositionAdded.bind(this);
|
this.onCompositionAdded = this.onCompositionAdded.bind(this);
|
||||||
this.onCompositionRemoved = this.onCompositionRemoved.bind(this);
|
this.onCompositionRemoved = this.onCompositionRemoved.bind(this);
|
||||||
@ -93,7 +91,7 @@ class InMemorySearchProvider {
|
|||||||
|
|
||||||
this.searchTypes = this.openmct.objects.SEARCH_TYPES;
|
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);
|
this.scheduleForIndexing(rootObject.identifier);
|
||||||
|
|
||||||
@ -163,8 +161,6 @@ class InMemorySearchProvider {
|
|||||||
return this.localSearchForObjects(queryId, query, maxResults);
|
return this.localSearchForObjects(queryId, query, maxResults);
|
||||||
} else if (searchType === this.searchTypes.ANNOTATIONS) {
|
} else if (searchType === this.searchTypes.ANNOTATIONS) {
|
||||||
return this.localSearchForAnnotations(queryId, query, maxResults);
|
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) {
|
} else if (searchType === this.searchTypes.TAGS) {
|
||||||
return this.localSearchForTags(queryId, query, maxResults);
|
return this.localSearchForTags(queryId, query, maxResults);
|
||||||
} else {
|
} else {
|
||||||
@ -281,13 +277,6 @@ class InMemorySearchProvider {
|
|||||||
provider.index(domainObject);
|
provider.index(domainObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTagMutation(domainObject, newTags) {
|
|
||||||
domainObject.tags = newTags;
|
|
||||||
const provider = this;
|
|
||||||
|
|
||||||
provider.index(domainObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
onCompositionAdded(newDomainObjectToIndex) {
|
onCompositionAdded(newDomainObjectToIndex) {
|
||||||
const provider = this;
|
const provider = this;
|
||||||
// The object comes in as a mutable domain object, which has functions,
|
// The object comes in as a mutable domain object, which has functions,
|
||||||
@ -342,14 +331,6 @@ class InMemorySearchProvider {
|
|||||||
composition.on('remove', this.onCompositionRemoved);
|
composition.on('remove', this.onCompositionRemoved);
|
||||||
this.indexedCompositions[keyString] = composition;
|
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')) {
|
if ((keyString !== 'ROOT')) {
|
||||||
@ -581,43 +562,6 @@ class InMemorySearchProvider {
|
|||||||
this.onWorkerMessage(eventToReturn);
|
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) {
|
destroyObservers(observers) {
|
||||||
Object.entries(observers).forEach(([keyString, unobserve]) => {
|
Object.entries(observers).forEach(([keyString, unobserve]) => {
|
||||||
if (typeof unobserve === 'function') {
|
if (typeof unobserve === 'function') {
|
||||||
|
@ -43,8 +43,6 @@
|
|||||||
port.postMessage(searchForAnnotations(event.data));
|
port.postMessage(searchForAnnotations(event.data));
|
||||||
} else if (requestType === 'TAGS') {
|
} else if (requestType === 'TAGS') {
|
||||||
port.postMessage(searchForTags(event.data));
|
port.postMessage(searchForTags(event.data));
|
||||||
} else if (requestType === 'NOTEBOOK_ANNOTATIONS') {
|
|
||||||
port.postMessage(searchForNotebookAnnotations(event.data));
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown request ${event.data.request}`);
|
throw new Error(`Unknown request ${event.data.request}`);
|
||||||
}
|
}
|
||||||
@ -204,33 +202,4 @@
|
|||||||
|
|
||||||
return message;
|
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
|
* to load domain objects
|
||||||
* @memberof module:openmct
|
* @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.
|
* Utilities for loading, saving, and manipulating domain objects.
|
||||||
* @interface ObjectAPI
|
* @interface ObjectAPI
|
||||||
@ -76,7 +85,6 @@ export default class ObjectAPI {
|
|||||||
this.SEARCH_TYPES = Object.freeze({
|
this.SEARCH_TYPES = Object.freeze({
|
||||||
OBJECTS: 'OBJECTS',
|
OBJECTS: 'OBJECTS',
|
||||||
ANNOTATIONS: 'ANNOTATIONS',
|
ANNOTATIONS: 'ANNOTATIONS',
|
||||||
NOTEBOOK_ANNOTATIONS: 'NOTEBOOK_ANNOTATIONS',
|
|
||||||
TAGS: 'TAGS'
|
TAGS: 'TAGS'
|
||||||
});
|
});
|
||||||
this.eventEmitter = new EventEmitter();
|
this.eventEmitter = new EventEmitter();
|
||||||
@ -188,7 +196,6 @@ export default class ObjectAPI {
|
|||||||
* @returns {Promise} a promise which will resolve when the domain object
|
* @returns {Promise} a promise which will resolve when the domain object
|
||||||
* has been saved, or be rejected if it cannot be saved
|
* has been saved, or be rejected if it cannot be saved
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get(identifier, abortSignal) {
|
get(identifier, abortSignal) {
|
||||||
let keystring = this.makeKeyString(identifier);
|
let keystring = this.makeKeyString(identifier);
|
||||||
|
|
||||||
@ -223,7 +230,7 @@ export default class ObjectAPI {
|
|||||||
if (result.isMutable) {
|
if (result.isMutable) {
|
||||||
result.$refresh(result);
|
result.$refresh(result);
|
||||||
} else {
|
} else {
|
||||||
let mutableDomainObject = this._toMutable(result);
|
let mutableDomainObject = this.toMutable(result);
|
||||||
mutableDomainObject.$refresh(result);
|
mutableDomainObject.$refresh(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +307,7 @@ export default class ObjectAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.get(identifier).then((object) => {
|
return this.get(identifier).then((object) => {
|
||||||
return this._toMutable(object);
|
return this.toMutable(object);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,7 +497,7 @@ export default class ObjectAPI {
|
|||||||
} else {
|
} else {
|
||||||
//Creating a temporary mutable domain object allows other mutable instances of the
|
//Creating a temporary mutable domain object allows other mutable instances of the
|
||||||
//object to be kept in sync.
|
//object to be kept in sync.
|
||||||
let mutableDomainObject = this._toMutable(domainObject);
|
let mutableDomainObject = this.toMutable(domainObject);
|
||||||
|
|
||||||
//Mutate original object
|
//Mutate original object
|
||||||
MutableDomainObject.mutateObject(domainObject, path, value);
|
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;
|
let mutableObject;
|
||||||
|
|
||||||
if (object.isMutable) {
|
if (domainObject.isMutable) {
|
||||||
mutableObject = object;
|
mutableObject = domainObject;
|
||||||
} else {
|
} else {
|
||||||
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
|
mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);
|
||||||
|
|
||||||
// Check if provider supports realtime updates
|
// Check if provider supports realtime updates
|
||||||
let identifier = utils.parseKeyString(mutableObject.identifier);
|
let identifier = utils.parseKeyString(mutableObject.identifier);
|
||||||
@ -526,9 +537,11 @@ export default class ObjectAPI {
|
|||||||
|
|
||||||
if (provider !== undefined
|
if (provider !== undefined
|
||||||
&& provider.observe !== undefined
|
&& provider.observe !== undefined
|
||||||
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
|
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
|
||||||
let unobserve = provider.observe(identifier, (updatedModel) => {
|
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
|
//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.
|
//in rapid succession and intermediate persistence states are returned by the observe function.
|
||||||
updatedModel = this.applyGetInterceptors(identifier, updatedModel);
|
updatedModel = this.applyGetInterceptors(identifier, updatedModel);
|
||||||
@ -582,7 +595,7 @@ export default class ObjectAPI {
|
|||||||
if (domainObject.isMutable) {
|
if (domainObject.isMutable) {
|
||||||
return domainObject.$observe(path, callback);
|
return domainObject.$observe(path, callback);
|
||||||
} else {
|
} else {
|
||||||
let mutable = this._toMutable(domainObject);
|
let mutable = this.toMutable(domainObject);
|
||||||
mutable.$observe(path, callback);
|
mutable.$observe(path, callback);
|
||||||
|
|
||||||
return () => mutable.$destroy();
|
return () => mutable.$destroy();
|
||||||
@ -675,8 +688,10 @@ export default class ObjectAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#hasAlreadyBeenPersisted(domainObject) {
|
#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
|
const result = domainObject.persisted !== undefined
|
||||||
&& domainObject.persisted >= domainObject.modified;
|
&& domainObject.persisted >= modified;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -320,7 +320,7 @@ describe("The Object API", () => {
|
|||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
// Duplicate object to guarantee we are not sharing object instance, which would invalidate test
|
// Duplicate object to guarantee we are not sharing object instance, which would invalidate test
|
||||||
testObjectDuplicate = JSON.parse(JSON.stringify(testObject));
|
testObjectDuplicate = JSON.parse(JSON.stringify(testObject));
|
||||||
mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate);
|
mutableSecondInstance = objectAPI.toMutable(testObjectDuplicate);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -151,6 +151,7 @@
|
|||||||
:key="entry.id"
|
:key="entry.id"
|
||||||
:entry="entry"
|
:entry="entry"
|
||||||
:domain-object="domainObject"
|
:domain-object="domainObject"
|
||||||
|
:notebook-annotations="notebookAnnotations[entry.id]"
|
||||||
:selected-page="selectedPage"
|
:selected-page="selectedPage"
|
||||||
:selected-section="selectedSection"
|
:selected-section="selectedSection"
|
||||||
:read-only="false"
|
:read-only="false"
|
||||||
@ -219,10 +220,12 @@ export default {
|
|||||||
isRestricted: this.domainObject.type === RESTRICTED_NOTEBOOK_TYPE,
|
isRestricted: this.domainObject.type === RESTRICTED_NOTEBOOK_TYPE,
|
||||||
search: '',
|
search: '',
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
|
lastLocalAnnotationCreation: 0,
|
||||||
showTime: this.domainObject.configuration.showTime || 0,
|
showTime: this.domainObject.configuration.showTime || 0,
|
||||||
showNav: false,
|
showNav: false,
|
||||||
sidebarCoversEntries: false,
|
sidebarCoversEntries: false,
|
||||||
filteredAndSortedEntries: []
|
filteredAndSortedEntries: [],
|
||||||
|
notebookAnnotations: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -289,7 +292,8 @@ export default {
|
|||||||
this.getSearchResults = debounce(this.getSearchResults, 500);
|
this.getSearchResults = debounce(this.getSearchResults, 500);
|
||||||
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
|
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
|
await this.loadAnnotations();
|
||||||
this.formatSidebar();
|
this.formatSidebar();
|
||||||
this.setSectionAndPageFromUrl();
|
this.setSectionAndPageFromUrl();
|
||||||
|
|
||||||
@ -307,6 +311,13 @@ export default {
|
|||||||
this.unobserveEntries();
|
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('orientationchange', this.formatSidebar);
|
||||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
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() {
|
filterAndSortEntries() {
|
||||||
const filterTime = Date.now();
|
const filterTime = Date.now();
|
||||||
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
|
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
|
||||||
@ -350,6 +387,10 @@ export default {
|
|||||||
this.filteredAndSortedEntries = this.defaultSort === 'oldest'
|
this.filteredAndSortedEntries = this.defaultSort === 'oldest'
|
||||||
? filteredPageEntriesByTime
|
? filteredPageEntriesByTime
|
||||||
: [...filteredPageEntriesByTime].reverse();
|
: [...filteredPageEntriesByTime].reverse();
|
||||||
|
|
||||||
|
if (this.lastLocalAnnotationCreation < this.domainObject.annotationLastCreated) {
|
||||||
|
this.loadAnnotations();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
changeSelectedSection({ sectionId, pageId }) {
|
changeSelectedSection({ sectionId, pageId }) {
|
||||||
const sections = this.sections.map(s => {
|
const sections = this.sections.map(s => {
|
||||||
@ -473,14 +514,8 @@ export default {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async removeAnnotations(entryId) {
|
removeAnnotations(entryId) {
|
||||||
const targetKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
this.openmct.annotation.deleteAnnotations(this.notebookAnnotations[entryId]);
|
||||||
const query = {
|
|
||||||
targetKeyString,
|
|
||||||
entryId
|
|
||||||
};
|
|
||||||
const existingAnnotation = await this.openmct.annotation.getAnnotation(query, this.openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
|
|
||||||
this.openmct.annotation.removeAnnotationTags(existingAnnotation);
|
|
||||||
},
|
},
|
||||||
checkEntryPos(entry) {
|
checkEntryPos(entry) {
|
||||||
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
|
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
|
||||||
|
@ -84,9 +84,8 @@
|
|||||||
|
|
||||||
<TagEditor
|
<TagEditor
|
||||||
:domain-object="domainObject"
|
:domain-object="domainObject"
|
||||||
:annotation-query="annotationQuery"
|
:annotations="notebookAnnotations"
|
||||||
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
||||||
:annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
|
|
||||||
:target-specific-details="{entryId: entry.id}"
|
:target-specific-details="{entryId: entry.id}"
|
||||||
@tags-updated="timestampAndUpdate"
|
@tags-updated="timestampAndUpdate"
|
||||||
/>
|
/>
|
||||||
@ -163,6 +162,12 @@ export default {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
notebookAnnotations: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
entry: {
|
entry: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
@ -204,15 +209,6 @@ export default {
|
|||||||
createdOnDate() {
|
createdOnDate() {
|
||||||
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
|
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() {
|
createdOnTime() {
|
||||||
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
||||||
},
|
},
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
|
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
|
||||||
<button
|
<button
|
||||||
class="c-icon-button c-icon-button--major icon-plus"
|
class="c-icon-button c-icon-button--major icon-plus"
|
||||||
|
aria-label="Add Section"
|
||||||
@click="addSection"
|
@click="addSection"
|
||||||
>
|
>
|
||||||
<span class="c-list-button__label">Add</span>
|
<span class="c-list-button__label">Add</span>
|
||||||
@ -33,6 +34,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="c-icon-button c-icon-button--major icon-plus"
|
class="c-icon-button c-icon-button--major icon-plus"
|
||||||
|
aria-label="Add Page"
|
||||||
@click="addPage"
|
@click="addPage"
|
||||||
>
|
>
|
||||||
<span class="c-icon-button__label">Add</span>
|
<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) {
|
export default function (openmct) {
|
||||||
const apiSave = openmct.objects.save.bind(openmct.objects);
|
const apiSave = openmct.objects.save.bind(openmct.objects);
|
||||||
|
|
||||||
openmct.objects.save = async (domainObject) => {
|
openmct.objects.save = async (domainObject) => {
|
||||||
if (!isNotebookType(domainObject)) {
|
if (!isNotebookOrAnnotationType(domainObject)) {
|
||||||
return apiSave(domainObject);
|
return apiSave(domainObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNewMutable = !domainObject.isMutable;
|
const isNewMutable = !domainObject.isMutable;
|
||||||
const localMutable = openmct.objects._toMutable(domainObject);
|
const localMutable = openmct.objects.toMutable(domainObject);
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await apiSave(localMutable);
|
result = await apiSave(localMutable);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof openmct.objects.errors.Conflict) {
|
if (error instanceof openmct.objects.errors.Conflict) {
|
||||||
result = resolveConflicts(localMutable, openmct);
|
result = await resolveConflicts(domainObject, localMutable, openmct);
|
||||||
} else {
|
} else {
|
||||||
result = Promise.reject(error);
|
result = Promise.reject(error);
|
||||||
}
|
}
|
||||||
@ -30,16 +31,56 @@ export default function (openmct) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveConflicts(localMutable, openmct) {
|
function resolveConflicts(domainObject, localMutable, openmct) {
|
||||||
const localEntries = JSON.parse(JSON.stringify(localMutable.configuration.entries));
|
if (isNotebookType(domainObject)) {
|
||||||
|
return resolveNotebookEntryConflicts(localMutable, openmct);
|
||||||
|
} else if (isAnnotationType(domainObject)) {
|
||||||
|
return resolveNotebookTagConflicts(localMutable, openmct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
|
async function resolveNotebookTagConflicts(localAnnotation, openmct) {
|
||||||
applyLocalEntries(remoteMutable, localEntries, 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) {
|
function applyLocalEntries(mutable, entries, openmct) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export const NOTEBOOK_TYPE = 'notebook';
|
export const NOTEBOOK_TYPE = 'notebook';
|
||||||
export const RESTRICTED_NOTEBOOK_TYPE = 'restricted-notebook';
|
export const RESTRICTED_NOTEBOOK_TYPE = 'restricted-notebook';
|
||||||
|
export const ANNOTATION_TYPE = 'annotation';
|
||||||
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
|
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
|
||||||
export const NOTEBOOK_DEFAULT = 'DEFAULT';
|
export const NOTEBOOK_DEFAULT = 'DEFAULT';
|
||||||
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
|
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
|
||||||
@ -9,10 +10,18 @@ export const NOTEBOOK_INSTALLED_KEY = '_NOTEBOOK_PLUGIN_INSTALLED';
|
|||||||
export const RESTRICTED_NOTEBOOK_INSTALLED_KEY = '_RESTRICTED_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
|
// 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) {
|
export function isNotebookType(domainObject) {
|
||||||
return [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE].includes(domainObject.type);
|
return [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE].includes(domainObject.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAnnotationType(domainObject) {
|
||||||
|
return [ANNOTATION_TYPE].includes(domainObject.type);
|
||||||
|
}
|
||||||
|
|
||||||
export function isNotebookViewType(view) {
|
export function isNotebookViewType(view) {
|
||||||
return [NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_VIEW_TYPE].includes(view);
|
return [NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_VIEW_TYPE].includes(view);
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ export default class AbstractStatusIndicator {
|
|||||||
#configuration;
|
#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
|
* @param {import('@/api/user/UserAPI').UserAPIConfiguration} configuration Per-deployment status styling. See the type definition in UserAPI
|
||||||
*/
|
*/
|
||||||
constructor(openmct, configuration) {
|
constructor(openmct, configuration) {
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
import CouchDocument from "./CouchDocument";
|
import CouchDocument from "./CouchDocument";
|
||||||
import CouchObjectQueue from "./CouchObjectQueue";
|
import CouchObjectQueue from "./CouchObjectQueue";
|
||||||
import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator";
|
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 REV = "_rev";
|
||||||
const ID = "_id";
|
const ID = "_id";
|
||||||
@ -71,7 +71,7 @@ class CouchObjectProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSharedWorkerMessageError(event) {
|
onSharedWorkerMessageError(event) {
|
||||||
console.log('Error', event);
|
console.error('Error', event);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSynchronizedObject(object) {
|
isSynchronizedObject(object) {
|
||||||
@ -290,7 +290,7 @@ class CouchObjectProvider {
|
|||||||
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
|
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
|
//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.
|
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
|
||||||
this.objectQueue[key].updateRevision(response[REV]);
|
this.objectQueue[key].updateRevision(response[REV]);
|
||||||
|
@ -31,7 +31,7 @@ class CouchSearchProvider {
|
|||||||
constructor(couchObjectProvider) {
|
constructor(couchObjectProvider) {
|
||||||
this.couchObjectProvider = couchObjectProvider;
|
this.couchObjectProvider = couchObjectProvider;
|
||||||
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
|
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) {
|
supportsSearchType(searchType) {
|
||||||
@ -43,8 +43,6 @@ class CouchSearchProvider {
|
|||||||
return this.searchForObjects(query, abortSignal);
|
return this.searchForObjects(query, abortSignal);
|
||||||
} else if (searchType === this.searchTypes.ANNOTATIONS) {
|
} else if (searchType === this.searchTypes.ANNOTATIONS) {
|
||||||
return this.searchForAnnotations(query, abortSignal);
|
return this.searchForAnnotations(query, abortSignal);
|
||||||
} else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
|
|
||||||
return this.searchForNotebookAnnotations(query, abortSignal);
|
|
||||||
} else if (searchType === this.searchTypes.TAGS) {
|
} else if (searchType === this.searchTypes.TAGS) {
|
||||||
return this.searchForTags(query, abortSignal);
|
return this.searchForTags(query, abortSignal);
|
||||||
} else {
|
} else {
|
||||||
@ -91,38 +89,6 @@ class CouchSearchProvider {
|
|||||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
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) {
|
searchForTags(tagsArray, abortSignal) {
|
||||||
const filter = {
|
const filter = {
|
||||||
"selector": {
|
"selector": {
|
||||||
@ -130,7 +96,8 @@ class CouchSearchProvider {
|
|||||||
{
|
{
|
||||||
"model.tags": {
|
"model.tags": {
|
||||||
"$elemMatch": {
|
"$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);
|
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||||
}
|
}
|
||||||
|
@ -244,7 +244,7 @@ define(
|
|||||||
|
|
||||||
if (context.item && context.item.isMutable !== true) {
|
if (context.item && context.item.isMutable !== true) {
|
||||||
removeMutable = true;
|
removeMutable = true;
|
||||||
context.item = this.openmct.objects._toMutable(context.item);
|
context.item = this.openmct.objects.toMutable(context.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (select) {
|
if (select) {
|
||||||
|
@ -51,18 +51,14 @@ export default {
|
|||||||
},
|
},
|
||||||
inject: ['openmct'],
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
annotationQuery: {
|
annotations: {
|
||||||
type: Object,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
annotationType: {
|
annotationType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
annotationSearchType: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
targetSpecificDetails: {
|
targetSpecificDetails: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@ -76,7 +72,6 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
annontation: null,
|
|
||||||
addedTags: [],
|
addedTags: [],
|
||||||
userAddingTag: false
|
userAddingTag: false
|
||||||
};
|
};
|
||||||
@ -92,57 +87,50 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
annotation: {
|
annotations: {
|
||||||
handler() {
|
handler() {
|
||||||
this.tagsChanged(this.annotation.tags);
|
this.annotationsChanged();
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
},
|
|
||||||
annotationQuery: {
|
|
||||||
handler() {
|
|
||||||
this.unloadAnnotation();
|
|
||||||
this.loadAnnotation();
|
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadAnnotation();
|
this.annotationsChanged();
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
if (this.removeTagsListener) {
|
|
||||||
this.removeTagsListener();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addAnnotationListener(annotation) {
|
annotationsChanged() {
|
||||||
if (annotation && !this.removeTagsListener) {
|
if (this.annotations && this.annotations.length) {
|
||||||
this.removeTagsListener = this.openmct.objects.observe(annotation, '*', (newAnnotation) => {
|
this.tagsChanged();
|
||||||
this.tagsChanged(newAnnotation.tags);
|
|
||||||
this.annotation = newAnnotation;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadAnnotation() {
|
annotationDeletionListener(changedAnnotation) {
|
||||||
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
|
const matchingAnnotation = this.annotations.find((possibleMatchingAnnotation) => {
|
||||||
this.addAnnotationListener(this.annotation);
|
return this.openmct.objects.areIdsEqual(possibleMatchingAnnotation.identifier, changedAnnotation.identifier);
|
||||||
if (this.annotation && this.annotation.tags) {
|
});
|
||||||
this.tagsChanged(this.annotation.tags);
|
if (matchingAnnotation) {
|
||||||
|
matchingAnnotation._deleted = changedAnnotation._deleted;
|
||||||
|
this.userAddingTag = false;
|
||||||
|
this.tagsChanged();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
unloadAnnotation() {
|
tagsChanged() {
|
||||||
if (this.removeTagsListener) {
|
// gather tags from annotations
|
||||||
this.removeTagsListener();
|
const tagsFromAnnotations = this.annotations.flatMap((annotation) => {
|
||||||
this.removeTagsListener = undefined;
|
if (annotation._deleted) {
|
||||||
}
|
return [];
|
||||||
},
|
} else {
|
||||||
tagsChanged(newTags) {
|
return annotation.tags;
|
||||||
if (newTags.length < this.addedTags.length) {
|
}
|
||||||
this.addedTags = this.addedTags.slice(0, newTags.length);
|
}).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) {
|
for (let index = 0; index < tagsFromAnnotations.length; index += 1) {
|
||||||
this.$set(this.addedTags, index, newTags[index]);
|
this.$set(this.addedTags, index, tagsFromAnnotations[index]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addTag() {
|
addTag() {
|
||||||
@ -153,23 +141,27 @@ export default {
|
|||||||
this.userAddingTag = true;
|
this.userAddingTag = true;
|
||||||
},
|
},
|
||||||
async tagRemoved(tagToRemove) {
|
async tagRemoved(tagToRemove) {
|
||||||
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
// Soft delete annotations that match tag instead
|
||||||
this.$emit('tags-updated');
|
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;
|
return result;
|
||||||
},
|
},
|
||||||
async tagAdded(newTag) {
|
async tagAdded(newTag) {
|
||||||
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
|
// Either undelete an annotation, or create one (1) new annotation
|
||||||
this.annotation = await this.openmct.annotation.addAnnotationTag(this.annotation,
|
const existingAnnotation = this.annotations.find((annotation) => {
|
||||||
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
|
return annotation.tags.includes(newTag);
|
||||||
if (annotationWasCreated) {
|
});
|
||||||
this.addAnnotationListener(this.annotation);
|
|
||||||
}
|
const createdAnnotation = await this.openmct.annotation.addSingleAnnotationTag(existingAnnotation,
|
||||||
|
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
|
||||||
|
|
||||||
this.tagsChanged(this.annotation.tags);
|
|
||||||
this.userAddingTag = false;
|
this.userAddingTag = false;
|
||||||
|
|
||||||
this.$emit('tags-updated');
|
this.$emit('tags-updated', createdAnnotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -37,7 +37,10 @@
|
|||||||
class="c-tag"
|
class="c-tag"
|
||||||
:style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
|
:style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
|
||||||
>
|
>
|
||||||
<div class="c-tag__label">{{ selectedTagLabel }} </div>
|
<div
|
||||||
|
class="c-tag__label"
|
||||||
|
aria-label="Tag"
|
||||||
|
>{{ selectedTagLabel }} </div>
|
||||||
<button
|
<button
|
||||||
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
|
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
|
||||||
@click="removeTag"
|
@click="removeTag"
|
||||||
|
@ -19,7 +19,7 @@ module.exports = merge(common, {
|
|||||||
// See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
|
// See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
|
||||||
ignored: [
|
ignored: [
|
||||||
'**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e,
|
'**/{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
|
'**/*.{sh,md,png,ttf,woff,svg}', // Non source files
|
||||||
'**/.*' // dotfiles and dotfolders
|
'**/.*' // dotfiles and dotfolders
|
||||||
]
|
]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user