mirror of
https://github.com/nasa/openmct.git
synced 2025-02-20 09:26:45 +00:00
Add markdown to notebook entries (#7084)
* try marked out * fix url validation * now rendering blockquotes properly * add abbrv, link titles, and strikethrough * fix tests and lint * Closes #6060 - CSS resets and styling for markdown-related HTML markup in Notebook entries. - Better styling and cursor affordances for Notebook entry selection and editing interaction flow. * add line breaks option * Closes #6060 - Tab * Closes #6060 - Conversion of contenteditable-div to textarea started. - Stubbed in textarea with styles. * have it markdown with a textarea and adjust size automatically * Closes #6060 - Padding added back to text `div` area. * Closes #6060 - Styles added to support Shift Log and hover behavior for entries on locked pages. - Removed `--major` styling from Shift Log Commit Entries button to reduce confusion with entry commit button. - CSS code cleanups. * two step focus/edit. also scroll into view for editing * add markdown, strip all tags, and truncate * lint * remove unneeded code * fix notebook entry, selected page may also be null * fix existing notebook tests * lint * fix whitelist * readd whitelist * lint * fix link tests * fix tests * fix tagging test * add some markdown test * get rid of pause * add another sanitization step --------- Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com> Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
This commit is contained in:
parent
2243381d52
commit
6947c912a7
@ -34,7 +34,8 @@ async function enterTextEntry(page, text) {
|
||||
await page.locator(NOTEBOOK_DROP_AREA).click();
|
||||
|
||||
// enter text
|
||||
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').fill(text);
|
||||
await page.getByLabel('Notebook Entry Display').last().click();
|
||||
await page.getByLabel('Notebook Entry Input').last().fill(text);
|
||||
await commitEntry(page);
|
||||
}
|
||||
|
||||
@ -52,7 +53,6 @@ async function dragAndDropEmbed(page, notebookObject) {
|
||||
await page.click('button[title="Show selected item in tree"]');
|
||||
// Drag and drop the SWG into the notebook
|
||||
await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA);
|
||||
await commitEntry(page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,8 +279,8 @@ test.describe('Notebook entry tests', () => {
|
||||
|
||||
// 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/);
|
||||
await expect(page.getByLabel('Notebook Entry Display')).toBeVisible();
|
||||
await expect(page.getByLabel('Notebook Entry', { exact: true })).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
|
||||
@ -369,6 +369,8 @@ test.describe('Notebook entry tests', () => {
|
||||
|
||||
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
|
||||
|
||||
expect(await validLink.count()).toBe(1);
|
||||
|
||||
// Start waiting for popup before clicking. Note no await.
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
|
||||
@ -378,8 +380,6 @@ test.describe('Notebook entry tests', () => {
|
||||
// Wait for the popup to load.
|
||||
await popup.waitForLoadState();
|
||||
expect.soft(popup.url()).toContain('www.google.com');
|
||||
|
||||
expect(await validLink.count()).toBe(1);
|
||||
});
|
||||
test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({
|
||||
page
|
||||
@ -447,6 +447,8 @@ test.describe('Notebook entry tests', () => {
|
||||
|
||||
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
|
||||
|
||||
expect(await validLink.count()).toBe(1);
|
||||
|
||||
// Start waiting for popup before clicking. Note no await.
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
|
||||
@ -456,8 +458,6 @@ test.describe('Notebook entry tests', () => {
|
||||
// Wait for the popup to load.
|
||||
await popup.waitForLoadState();
|
||||
expect.soft(popup.url()).toContain('www.google.com');
|
||||
|
||||
expect(await validLink.count()).toBe(1);
|
||||
});
|
||||
test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({
|
||||
page
|
||||
@ -482,4 +482,42 @@ test.describe('Notebook entry tests', () => {
|
||||
expect.soft(await sanitizedLink.count()).toBe(1);
|
||||
expect(await unsanitizedLink.count()).toBe(0);
|
||||
});
|
||||
test('Can add markdown to a notebook entry', async ({ page }) => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Headers
|
||||
const headerMarkdown = `# Big Header\n## Large Header\n### Medium Header\n#### Small Header`;
|
||||
await nbUtils.enterTextEntry(page, headerMarkdown);
|
||||
await expect(page.getByRole('heading', { name: 'Big Header' })).toBeVisible();
|
||||
|
||||
// Text markup
|
||||
const markupText =
|
||||
'**This is bold.** _This is italic_. `This is code`. ~This is strikethrough~';
|
||||
await nbUtils.enterTextEntry(page, markupText);
|
||||
await expect(page.locator('strong:has-text("This is bold.")')).toBeVisible();
|
||||
|
||||
// Tables
|
||||
const tablesText = '|Col 1|Col 2|Col3|\n|-|-|-|\n |Value 1|Value 2|Value 3|\n';
|
||||
await nbUtils.enterTextEntry(page, tablesText);
|
||||
await expect(page.getByRole('cell', { name: 'Value 2' })).toBeVisible();
|
||||
|
||||
// Links
|
||||
const linksText =
|
||||
'Raw links https://www.google.com and Markdown links like [Google](https://www.google.com) work';
|
||||
await nbUtils.enterTextEntry(page, linksText);
|
||||
await expect(page.getByRole('link', { name: 'https://www.google.com' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Google', exact: true })).toBeVisible();
|
||||
|
||||
// Lists
|
||||
const listsText = '- List item 1\n - Item 1A \n- List Item 2\n 1. Order 1\n 1. Order 2\n';
|
||||
await nbUtils.enterTextEntry(page, listsText);
|
||||
const childItem = page.locator('li:has-text("List Item 2") ol li:has-text("Order 2")');
|
||||
await expect(childItem).toBeVisible();
|
||||
|
||||
// Blocks
|
||||
const blockTest = '```javascript\nconst foo = "bar";\nconst bar = "foo";\n```';
|
||||
await nbUtils.enterTextEntry(page, blockTest);
|
||||
const codeBlock = page.locator('code.language-javascript:has-text("const foo = \\"bar\\";")');
|
||||
await expect(codeBlock).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -192,8 +192,6 @@ test.describe('Snapshot image tests', () => {
|
||||
// be sure that entry was created
|
||||
await expect(page.getByText('favicon-96x96.png')).toBeVisible();
|
||||
|
||||
// click on image (need to click twice to focus)
|
||||
await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click();
|
||||
await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click();
|
||||
|
||||
// expect large image to be displayed
|
||||
|
@ -147,16 +147,14 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5823'
|
||||
});
|
||||
|
||||
await createNotebookEntryAndTags(page);
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = 1`;
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`An entry without tags`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
|
||||
await page.getByLabel('Notebook Entry Display').last().click();
|
||||
await page.getByLabel('Notebook Entry Input').fill(`An entry without tags`);
|
||||
await page.locator('.c-ne__save-button > button').click();
|
||||
|
||||
await page.hover('[aria-label="Notebook Entry Input"] >> nth=1');
|
||||
await page.hover('[aria-label="Notebook Entry Display"] >> nth=1');
|
||||
await page.locator('button[title="Delete this entry"]').last().click();
|
||||
await expect(
|
||||
page.locator('text=This action will permanently delete this entry. Do you wish to continue?')
|
||||
|
@ -131,8 +131,9 @@ test.describe('Performance tests', () => {
|
||||
await page.evaluate(() => window.performance.mark('new-notebook-entry-created'));
|
||||
|
||||
// Enter Notebook Entry text
|
||||
await page.locator('div.c-ne__text').last().fill('New Entry');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.getByLabel('Notebook Entry').last().click();
|
||||
await page.getByLabel('Notebook Entry Input').last().fill('New Entry');
|
||||
await page.locator('.c-ne__save-button').click();
|
||||
await page.evaluate(() => window.performance.mark('new-notebook-entry-filled'));
|
||||
|
||||
//Individual Notebook Entry Search
|
||||
|
@ -52,6 +52,7 @@
|
||||
"karma-webpack": "5.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "9.0.3",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
|
@ -19,7 +19,6 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="c-notebook" :class="[{ 'c-notebook--restricted': isRestricted }]">
|
||||
<div class="c-notebook__head">
|
||||
@ -62,7 +61,13 @@
|
||||
@selectSection="selectSection"
|
||||
@toggleNav="toggleNav"
|
||||
/>
|
||||
<div class="c-notebook__page-view">
|
||||
<div
|
||||
class="c-notebook__page-view"
|
||||
:class="{
|
||||
'c-notebook--page-locked': selectedPage?.isLocked,
|
||||
'c-notebook--page-unlocked': !selectedPage?.isLocked
|
||||
}"
|
||||
>
|
||||
<div class="c-notebook__page-view__header">
|
||||
<button
|
||||
class="c-notebook__toggle-nav-button c-icon-button c-icon-button--major icon-menu-hamburger"
|
||||
@ -107,9 +112,9 @@
|
||||
class="c-telemetry-table__progress-bar"
|
||||
:model="{ progressPerc: null }"
|
||||
/>
|
||||
<div v-if="selectedPage && selectedPage.isLocked" class="c-notebook__page-locked">
|
||||
<div v-if="selectedPage && selectedPage.isLocked" class="c-notebook__page-locked-message">
|
||||
<div class="icon-lock"></div>
|
||||
<div class="c-notebook__page-locked__message">
|
||||
<div class="c-notebook__page-locked-message-text">
|
||||
This page has been committed and cannot be modified or removed
|
||||
</div>
|
||||
</div>
|
||||
@ -142,7 +147,7 @@
|
||||
class="c-notebook__commit-entries-control"
|
||||
>
|
||||
<button
|
||||
class="c-button c-button--major commit-button icon-lock"
|
||||
class="c-button commit-button icon-lock"
|
||||
title="Commit entries and lock this page from further changes"
|
||||
@click="lockPage()"
|
||||
>
|
||||
@ -185,6 +190,7 @@ import {
|
||||
import NotebookEntry from './NotebookEntry.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
|
||||
function objectCopy(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="entry"
|
||||
class="c-notebook__entry c-ne has-local-controls"
|
||||
aria-label="Notebook Entry"
|
||||
:class="{ locked: isLocked, 'is-selected': isSelectedEntry, 'is-editing': editMode }"
|
||||
@ -56,7 +57,7 @@
|
||||
<template v-if="readOnly && result">
|
||||
<div :id="entry.id" class="c-ne__text highlight" tabindex="0">
|
||||
<TextHighlight
|
||||
:text="formatValidUrls(entry.text)"
|
||||
:text="convertMarkDownToHtml(entry.text)"
|
||||
:highlight="highlightText"
|
||||
:highlight-class="'search-highlight'"
|
||||
/>
|
||||
@ -64,18 +65,26 @@
|
||||
</template>
|
||||
<template v-else-if="!isLocked">
|
||||
<div
|
||||
v-if="!editMode"
|
||||
v-bind.prop="formattedText"
|
||||
:id="entry.id"
|
||||
class="c-ne__text c-ne__input"
|
||||
tabindex="-1"
|
||||
aria-label="Notebook Entry Display"
|
||||
class="c-ne__text"
|
||||
@mouseover="checkEditability($event)"
|
||||
@click="editingEntry($event)"
|
||||
></div>
|
||||
<textarea
|
||||
v-else
|
||||
:id="entry.id"
|
||||
ref="entryInput"
|
||||
v-model="entry.text"
|
||||
class="c-ne__input"
|
||||
aria-label="Notebook Entry Input"
|
||||
tabindex="-1"
|
||||
:contenteditable="canEdit"
|
||||
@mouseover="checkEditability($event)"
|
||||
@mouseleave="canEdit = true"
|
||||
@mousedown="preventFocusIfNotSelected($event)"
|
||||
@focus="editingEntry()"
|
||||
@blur="updateEntryValue($event)"
|
||||
></div>
|
||||
></textarea>
|
||||
<div v-if="editMode" class="c-ne__save-button">
|
||||
<button class="c-button c-button--major icon-check"></button>
|
||||
</div>
|
||||
@ -139,6 +148,7 @@
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import { Marked } from 'marked';
|
||||
import Moment from 'moment';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
@ -151,11 +161,49 @@ import {
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
|
||||
const SANITIZATION_SCHEMA = {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {}
|
||||
allowedTags: [
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'blockquote',
|
||||
'p',
|
||||
'a',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'b',
|
||||
'i',
|
||||
'strong',
|
||||
'em',
|
||||
's',
|
||||
'strike',
|
||||
'code',
|
||||
'hr',
|
||||
'br',
|
||||
'div',
|
||||
'table',
|
||||
'thead',
|
||||
'caption',
|
||||
'tbody',
|
||||
'tr',
|
||||
'th',
|
||||
'td',
|
||||
'pre',
|
||||
'del',
|
||||
'ins',
|
||||
'mark',
|
||||
'abbr'
|
||||
],
|
||||
allowedAttributes: {
|
||||
a: ['href', 'target', 'class', 'title'],
|
||||
code: ['class'],
|
||||
abbr: ['title']
|
||||
}
|
||||
};
|
||||
const URL_REGEX =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
|
||||
|
||||
const UNKNOWN_USER = 'Unknown';
|
||||
|
||||
export default {
|
||||
@ -236,16 +284,15 @@ export default {
|
||||
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
||||
},
|
||||
formattedText() {
|
||||
// remove ANY tags
|
||||
const text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA);
|
||||
const text = this.entry.text;
|
||||
|
||||
if (this.editMode || this.urlWhitelist.length === 0) {
|
||||
if (this.editMode) {
|
||||
return { innerText: text };
|
||||
}
|
||||
|
||||
const html = this.formatValidUrls(text);
|
||||
const markDownHtml = this.convertMarkDownToHtml(text);
|
||||
|
||||
return { innerHTML: html };
|
||||
return { innerHTML: markDownHtml };
|
||||
},
|
||||
isSelectedEntry() {
|
||||
return this.selectedEntryId === this.entry.id;
|
||||
@ -276,7 +323,23 @@ export default {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editMode() {
|
||||
this.$nextTick(() => {
|
||||
// waiting for textarea to be rendered
|
||||
this.$refs.entryInput?.focus();
|
||||
this.adjustTextareaHeight();
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.marked = new Marked();
|
||||
this.renderer = new this.marked.Renderer();
|
||||
},
|
||||
mounted() {
|
||||
const originalLinkRenderer = this.renderer.link;
|
||||
this.renderer.link = this.validateLink.bind(this, originalLinkRenderer);
|
||||
|
||||
this.manageEmbedLayout = _.debounce(this.manageEmbedLayout, 400);
|
||||
|
||||
if (this.$refs.embedsWrapper) {
|
||||
@ -309,6 +372,41 @@ export default {
|
||||
|
||||
this.manageEmbedLayout();
|
||||
},
|
||||
convertMarkDownToHtml(text) {
|
||||
let markDownHtml = this.marked.parse(text, {
|
||||
breaks: true,
|
||||
renderer: this.renderer
|
||||
});
|
||||
markDownHtml = sanitizeHtml(markDownHtml, SANITIZATION_SCHEMA);
|
||||
return markDownHtml;
|
||||
},
|
||||
adjustTextareaHeight() {
|
||||
if (this.$refs.entryInput) {
|
||||
this.$refs.entryInput.style.height = 'auto';
|
||||
this.$refs.entryInput.style.height = `${this.$refs?.entryInput.scrollHeight}px`;
|
||||
this.$refs.entryInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
},
|
||||
validateLink(originalLinkRenderer, href, title, text) {
|
||||
try {
|
||||
const domain = new URL(href).hostname;
|
||||
const urlIsWhitelisted = this.urlWhitelist.some((partialDomain) => {
|
||||
return domain.endsWith(partialDomain);
|
||||
});
|
||||
if (!urlIsWhitelisted) {
|
||||
return text;
|
||||
}
|
||||
const linkHtml = originalLinkRenderer.call(this.renderer, href, title, text);
|
||||
const linkHtmlWithTarget = linkHtml.replace(
|
||||
/^<a /,
|
||||
'<a class="c-hyperlink" target="_blank"'
|
||||
);
|
||||
return linkHtmlWithTarget;
|
||||
} catch (error) {
|
||||
// had error parsing this URL, just return the text
|
||||
return text;
|
||||
}
|
||||
},
|
||||
cancelEditMode(event) {
|
||||
const isEditing = this.openmct.editor.isEditing();
|
||||
if (isEditing) {
|
||||
@ -333,22 +431,6 @@ export default {
|
||||
deleteEntry() {
|
||||
this.$emit('deleteEntry', this.entry.id);
|
||||
},
|
||||
formatValidUrls(text) {
|
||||
return text.replace(URL_REGEX, (match) => {
|
||||
const url = new URL(match);
|
||||
const domain = url.hostname;
|
||||
let result = match;
|
||||
let isMatch = this.urlWhitelist.find((partialDomain) => {
|
||||
return domain.endsWith(partialDomain);
|
||||
});
|
||||
|
||||
if (isMatch) {
|
||||
result = `<a class="c-hyperlink" target="_blank" href="${match}">${match}</a>`;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
},
|
||||
manageEmbedLayout() {
|
||||
if (this.$refs.embeds) {
|
||||
const embedsWrapperLength = this.$refs.embedsWrapper.clientWidth;
|
||||
@ -417,9 +499,6 @@ export default {
|
||||
|
||||
return position;
|
||||
},
|
||||
forceBlur(event) {
|
||||
event.target.blur();
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
},
|
||||
@ -474,29 +553,26 @@ export default {
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
preventFocusIfNotSelected($event) {
|
||||
if (!this.isSelectedEntry) {
|
||||
$event.preventDefault();
|
||||
// blur the previous focused entry if clicking on non selected entry input
|
||||
const focusedElementId = document.activeElement?.id;
|
||||
if (focusedElementId !== this.entry.id) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
editingEntry(event) {
|
||||
this.selectAndEmitEntry(event, this.entry);
|
||||
if (this.isSelectedEntry) {
|
||||
// selected and click, so we're ready to edit
|
||||
this.selectAndEmitEntry(event, this.entry);
|
||||
this.editMode = true;
|
||||
this.adjustTextareaHeight();
|
||||
this.$emit('editingEntry');
|
||||
}
|
||||
},
|
||||
editingEntry() {
|
||||
this.editMode = true;
|
||||
this.$emit('editingEntry');
|
||||
},
|
||||
updateEntryValue($event) {
|
||||
this.editMode = false;
|
||||
const value = $event.target.innerText;
|
||||
this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA);
|
||||
const rawEntryValue = $event.target.value;
|
||||
const sanitizeInput = sanitizeHtml(rawEntryValue, { allowedAttributes: [], allowedTags: [] });
|
||||
this.entry.text = sanitizeInput;
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
selectAndEmitEntry(event, entry) {
|
||||
selectEntry({
|
||||
element: event.currentTarget,
|
||||
element: this.$refs.entry,
|
||||
entryId: entry.id,
|
||||
domainObject: this.domainObject,
|
||||
openmct: this.openmct,
|
||||
|
@ -220,7 +220,7 @@ describe('Notebook plugin:', () => {
|
||||
const notebookEntryElements = element.querySelectorAll('.c-notebook__entry');
|
||||
const firstEntryText = getEntryText(0);
|
||||
expect(notebookEntryElements.length).toBe(2);
|
||||
expect(firstEntryText.innerText).toBe('First Test Entry');
|
||||
expect(firstEntryText.innerText.trim()).toBe('First Test Entry');
|
||||
});
|
||||
|
||||
describe('synchronization', () => {
|
||||
@ -232,13 +232,13 @@ describe('Notebook plugin:', () => {
|
||||
});
|
||||
|
||||
it('updates an entry when another user modifies it', () => {
|
||||
expect(getEntryText(0).innerText).toBe('First Test Entry');
|
||||
expect(getEntryText(0).innerText.trim()).toBe('First Test Entry');
|
||||
objectCloneToSyncFrom.configuration.entries['test-section-1']['test-page-1'][0].text =
|
||||
'Modified entry text';
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(getEntryText(0).innerText).toBe('Modified entry text');
|
||||
expect(getEntryText(0).innerText.trim()).toBe('Modified entry text');
|
||||
});
|
||||
});
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -53,6 +53,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Marked } from 'marked';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
import { identifierToString } from '../../../../src/tools/url';
|
||||
import ObjectPath from '../../components/ObjectPath.vue';
|
||||
import PreviewAction from '../../preview/PreviewAction';
|
||||
@ -81,30 +84,13 @@ export default {
|
||||
},
|
||||
getResultName() {
|
||||
if (this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK) {
|
||||
const targetID = Object.keys(this.result.targets)[0];
|
||||
const entryIdToFind = this.result.targets[targetID].entryId;
|
||||
const notebookModel = this.result.targetModels[0].configuration.entries;
|
||||
|
||||
const sections = Object.values(notebookModel);
|
||||
for (const section of sections) {
|
||||
const pages = Object.values(section);
|
||||
for (const entries of pages) {
|
||||
for (const entry of entries) {
|
||||
if (entry.id === entryIdToFind) {
|
||||
return entry.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Could not find any matching Notebook entries';
|
||||
const previewText = this.getNotebookPreviewText(this.result);
|
||||
return previewText;
|
||||
} else if (
|
||||
this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.GEOSPATIAL
|
||||
) {
|
||||
const targetID = Object.keys(this.result.targets)[0];
|
||||
const { layerName, name } = this.result.targets[targetID];
|
||||
|
||||
return layerName ? `${layerName} - ${name}` : name;
|
||||
const previewText = this.getGeospatialPreviewText(this.result);
|
||||
return previewText;
|
||||
} else {
|
||||
return this.result.targetModels[0].name;
|
||||
}
|
||||
@ -119,6 +105,9 @@ export default {
|
||||
return this.result.fullTagModels[0].foregroundColor;
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.marked = new Marked();
|
||||
},
|
||||
mounted() {
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
this.previewAction.on('isVisible', this.togglePreviewState);
|
||||
@ -129,6 +118,48 @@ export default {
|
||||
this.openmct.selection.off('change', this.fireAnnotationSelection);
|
||||
},
|
||||
methods: {
|
||||
getNotebookEntryTextById(entryIdToFind, notebookModel) {
|
||||
const sections = Object.values(notebookModel);
|
||||
for (const section of sections) {
|
||||
const pages = Object.values(section);
|
||||
for (const entries of pages) {
|
||||
for (const entry of entries) {
|
||||
if (entry.id === entryIdToFind) {
|
||||
return entry.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getNotebookPreviewText(result) {
|
||||
const targetID = Object.keys(this.result.targets)[0];
|
||||
const entryIdToFind = this.result.targets[targetID].entryId;
|
||||
const notebookModel = this.result.targetModels[0].configuration.entries;
|
||||
|
||||
const entryText = this.getNotebookEntryTextById(entryIdToFind, notebookModel);
|
||||
if (entryText === null) {
|
||||
return 'Could not find any matching Notebook entries';
|
||||
}
|
||||
const markDownHtml = this.marked.parse(entryText, {
|
||||
breaks: true
|
||||
});
|
||||
// strip everything
|
||||
const cleanedHtml = sanitizeHtml(markDownHtml, { allowedAttributes: [], allowedTags: [] });
|
||||
// strip to 64 characters
|
||||
let truncatedText = cleanedHtml.substring(0, 64);
|
||||
// add ellipsis if necessary
|
||||
if (truncatedText.length < entryText.length) {
|
||||
truncatedText = `${truncatedText}...`;
|
||||
}
|
||||
return truncatedText;
|
||||
},
|
||||
getGeospatialPreviewText(result) {
|
||||
const targetID = Object.keys(this.result.targets)[0];
|
||||
const { layerName, name } = this.result.targets[targetID];
|
||||
|
||||
return layerName ? `${layerName} - ${name}` : name;
|
||||
},
|
||||
clickedResult(event) {
|
||||
const objectPath = this.domainObject.originalPath;
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user