Ensure annotations on empty entries in notebook are not lost (#6525)

* entries now selected on creation

* select previous entry on deletion

* add deletion test

* wip

* fix adding focus selection

* remove previous entry selection logic

* null check for event

* address review comments

* address review comments

* refactor tests a bit

* typo

* remove clicking on entries
This commit is contained in:
Scott Bell 2023-04-04 23:37:38 +02:00 committed by GitHub
parent bc3a5408b4
commit 2e60da0401
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 68 deletions

View File

@ -198,6 +198,36 @@ test.describe('Notebook page tests', () => {
}); });
}); });
test.describe('Notebook export tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook
await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});
test('can export notebook as text', async ({ page }) => {
await nbUtils.enterTextEntry(page, `Foo bar entry`);
// Click on 3 Dot Menu
await page.locator('button[title="More options"]').click();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();
await page.getByRole('button', { name: 'Save' }).click();
const download = await downloadPromise;
const readStream = await download.createReadStream();
const exportedText = await streamToString(readStream);
expect(exportedText).toContain('Foo bar entry');
});
test.fixme('can export multiple notebook entries as text ', async ({ page }) => {});
test.fixme('can export all notebook entry metdata', async ({ page }) => {});
test.fixme('can export all notebook tags', async ({ page }) => {});
test.fixme('can export all notebook snapshots', async ({ page }) => {});
});
test.describe('Notebook search tests', () => { test.describe('Notebook search tests', () => {
test.fixme('Can search for a single result', async ({ page }) => {}); test.fixme('Can search for a single result', async ({ page }) => {});
test.fixme('Can search for many results', async ({ page }) => {}); test.fixme('Can search for many results', async ({ page }) => {});
@ -219,7 +249,15 @@ test.describe('Notebook entry tests', () => {
type: NOTEBOOK_NAME type: NOTEBOOK_NAME
}); });
}); });
test.fixme('When a new entry is created, it should be focused', async ({ page }) => {}); test('When a new entry is created, it should be focused and selected', async ({ page }) => {
// Navigate to the notebook object
await page.goto(notebookObject.url);
// Click .c-notebook__drag-area
await page.locator('.c-notebook__drag-area').click();
await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible();
await expect(page.locator('[aria-label="Notebook Entry"]')).toHaveClass(/is-selected/);
});
test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => { test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => {
// Create Overlay Plot // Create Overlay Plot
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
@ -263,7 +301,25 @@ test.describe('Notebook entry tests', () => {
expect(embedName).toBe('Dropped Overlay Plot'); expect(embedName).toBe('Dropped Overlay Plot');
}); });
test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
test.fixme('previous and new entries can be deleted', async ({ page }) => {}); test('previous and new entries can be deleted', async ({ page }) => {
// Navigate to the notebook object
await page.goto(notebookObject.url);
await nbUtils.enterTextEntry(page, 'First Entry');
await page.hover('text="First Entry"');
await page.click('button[title="Delete this entry"]');
await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click();
await expect(page.locator('text="First Entry"')).toBeHidden();
await nbUtils.enterTextEntry(page, 'Another First Entry');
await nbUtils.enterTextEntry(page, 'Second Entry');
await nbUtils.enterTextEntry(page, 'Third Entry');
await page.hover('[aria-label="Notebook Entry"] >> nth=2');
await page.click('button[title="Delete this entry"] >> nth=2');
await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click();
await expect(page.locator('text="Third Entry"')).toBeHidden();
await expect(page.locator('text="Another First Entry"')).toBeVisible();
await expect(page.locator('text="Second Entry"')).toBeVisible();
});
test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
const TEST_LINK = 'http://www.google.com'; const TEST_LINK = 'http://www.google.com';
@ -377,22 +433,4 @@ test.describe('Notebook entry tests', () => {
expect.soft(await sanitizedLink.count()).toBe(1); expect.soft(await sanitizedLink.count()).toBe(1);
expect(await unsanitizedLink.count()).toBe(0); expect(await unsanitizedLink.count()).toBe(0);
}); });
test('can export notebook as text', async ({ page }) => {
await nbUtils.enterTextEntry(page, `Foo bar entry`);
// Click on 3 Dot Menu
await page.locator('button[title="More options"]').click();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();
await page.getByRole('button', { name: 'Save' }).click();
const download = await downloadPromise;
const readStream = await download.createReadStream();
const exportedText = await streamToString(readStream);
expect(exportedText).toContain('Foo bar entry');
});
test.fixme('can export multiple notebook entries as text ', async ({ page }) => {});
test.fixme('can export all notebook entry metdata', async ({ page }) => {});
test.fixme('can export all notebook tags', async ({ page }) => {});
test.fixme('can export all notebook snapshots', async ({ page }) => {});
}); });

View File

@ -34,9 +34,6 @@ const nbUtils = require('../../../../helper/notebookUtils');
* @param {number} [iterations = 1] - the number of entries to create * @param {number} [iterations = 1] - the number of entries to create
*/ */
async function createNotebookAndEntry(page, iterations = 1) { async function createNotebookAndEntry(page, iterations = 1) {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' }); const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) { for (let iteration = 0; iteration < iterations; iteration++) {
@ -81,12 +78,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
} }
test.describe('Tagging in Notebooks @addInit', () => { test.describe('Tagging in Notebooks @addInit', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Can load tags', async ({ page }) => { test('Can load tags', async ({ page }) => {
await createNotebookAndEntry(page); await createNotebookAndEntry(page);
// TODO can be removed with fix for https://github.com/nasa/openmct/issues/6411
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').click();
await selectInspectorTab(page, 'Annotations'); await selectInspectorTab(page, 'Annotations');
await page.locator('button:has-text("Add Tag")').click(); await page.locator('button:has-text("Add Tag")').click();
@ -110,12 +108,24 @@ test.describe('Tagging in Notebooks @addInit', () => {
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
}); });
test('Can add tags with blank entry', async ({ page }) => {
createDomainObjectWithDefaults(page, { type: 'Notebook' });
await selectInspectorTab(page, 'Annotations');
await nbUtils.enterTextEntry(page, '');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
});
test('Can cancel adding tags', async ({ page }) => { test('Can cancel adding tags', async ({ page }) => {
await createNotebookAndEntry(page); await createNotebookAndEntry(page);
// TODO can be removed with fix for https://github.com/nasa/openmct/issues/6411
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').click();
await selectInspectorTab(page, 'Annotations'); await selectInspectorTab(page, 'Annotations');
// Test canceling adding a tag after we click "Type to select tag" // Test canceling adding a tag after we click "Type to select tag"
@ -270,9 +280,6 @@ test.describe('Tagging in Notebooks @addInit', () => {
test('Can cancel adding a tag', async ({ page }) => { test('Can cancel adding a tag', async ({ page }) => {
await createNotebookAndEntry(page); await createNotebookAndEntry(page);
// TODO can be removed with fix for https://github.com/nasa/openmct/issues/6411
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').click();
await selectInspectorTab(page, 'Annotations'); await selectInspectorTab(page, 'Annotations');
// Click on the "Add Tag" button // Click on the "Add Tag" button

View File

@ -125,7 +125,7 @@
v-if="selectedPage && !selectedPage.isLocked" v-if="selectedPage && !selectedPage.isLocked"
:class="{ 'disabled': activeTransaction }" :class="{ 'disabled': activeTransaction }"
class="c-notebook__drag-area icon-plus" class="c-notebook__drag-area icon-plus"
@click="newEntry()" @click="newEntry(null, $event)"
@dragover="dragOver" @dragover="dragOver"
@drop.capture="dropCapture" @drop.capture="dropCapture"
@drop="dropOnEntry($event)" @drop="dropOnEntry($event)"
@ -193,7 +193,7 @@ import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue'; import Sidebar from './Sidebar.vue';
import ProgressBar from '../../../ui/components/ProgressBar.vue'; import ProgressBar from '../../../ui/components/ProgressBar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage'; import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries'; import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject, selectEntry } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image'; import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import { isNotebookViewType, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants'; import { isNotebookViewType, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants';
@ -793,15 +793,29 @@ export default {
return section.id; return section.id;
}, },
async newEntry(embed = null) { async newEntry(embed, event) {
this.startTransaction(); this.startTransaction();
this.resetSearch(); this.resetSearch();
const notebookStorage = this.createNotebookStorageObject(); const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage); this.updateDefaultNotebook(notebookStorage);
const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed); const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
const element = this.$refs.notebookEntries.querySelector(`#${id}`);
const entryAnnotations = this.notebookAnnotations[id] ?? {};
selectEntry({
element,
entryId: id,
domainObject: this.domainObject,
openmct: this.openmct,
notebookAnnotations: entryAnnotations
});
if (event) {
event.stopPropagation();
}
this.filterAndSortEntries();
this.focusEntryId = id; this.focusEntryId = id;
this.selectedEntryId = id; this.selectedEntryId = id;
this.filterAndSortEntries();
}, },
orientationChange() { orientationChange() {
this.formatSidebar(); this.formatSidebar();

View File

@ -32,7 +32,7 @@
@dragover="changeCursor" @dragover="changeCursor"
@drop.capture="cancelEditMode" @drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry" @drop.prevent="dropOnEntry"
@click="selectEntry($event, entry)" @click="selectAndEmitEntry($event, entry)"
> >
<div class="c-ne__time-and-content"> <div class="c-ne__time-and-content">
<div class="c-ne__time-and-creator-and-delete"> <div class="c-ne__time-and-creator-and-delete">
@ -164,7 +164,7 @@
<script> <script>
import NotebookEmbed from './NotebookEmbed.vue'; import NotebookEmbed from './NotebookEmbed.vue';
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue'; import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
import { createNewEmbed } from '../utils/notebook-entries'; import { createNewEmbed, selectEntry } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image'; import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
@ -479,37 +479,18 @@ export default {
updateEntryValue($event) { updateEntryValue($event) {
this.editMode = false; this.editMode = false;
const value = $event.target.innerText; const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA); this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA);
this.timestampAndUpdate(); this.timestampAndUpdate();
} else {
this.$emit('cancelEdit');
}
}, },
selectEntry(event, entry) { selectAndEmitEntry(event, entry) {
const targetDetails = {}; selectEntry({
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
targetDetails[keyString] = {
entryId: entry.id
};
const targetDomainObjects = {};
targetDomainObjects[keyString] = this.domainObject;
this.openmct.selection.select(
[
{
element: event.currentTarget, element: event.currentTarget,
context: { entryId: entry.id,
type: 'notebook-entry-selection', domainObject: this.domainObject,
item: this.domainObject, openmct: this.openmct,
targetDetails, onAnnotationChange: this.timestampAndUpdate,
targetDomainObjects, notebookAnnotations: this.notebookAnnotations
annotations: this.notebookAnnotations, });
annotationType: this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
onAnnotationChange: this.timestampAndUpdate
}
}
],
false);
event.stopPropagation(); event.stopPropagation();
this.$emit('entry-selection', this.entry); this.$emit('entry-selection', this.entry);
} }

View File

@ -44,6 +44,35 @@ export function addEntryIntoPage(notebookStorage, entries, entry) {
return newEntries; return newEntries;
} }
export function selectEntry({
element, entryId, domainObject, openmct,
onAnnotationChange, notebookAnnotations
}) {
const targetDetails = {};
const keyString = openmct.objects.makeKeyString(domainObject.identifier);
targetDetails[keyString] = {
entryId
};
const targetDomainObjects = {};
targetDomainObjects[keyString] = domainObject;
openmct.selection.select(
[
{
element,
context: {
type: 'notebook-entry-selection',
item: domainObject,
targetDetails,
targetDomainObjects,
annotations: notebookAnnotations,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
onAnnotationChange
}
}
],
false);
}
export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) { export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) {
if (historicLink.includes('tc.mode=fixed')) { if (historicLink.includes('tc.mode=fixed')) {
return historicLink; return historicLink;