From c74b6c5d1fc87de01576a78f5eb24f2963d5bb82 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Thu, 5 Oct 2023 13:19:39 +0200 Subject: [PATCH 01/16] handle unknown files and deal with copy/paste --- .../notebook/components/NotebookEntry.vue | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 3101a2df3ee..55b703d83d6 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -31,6 +31,7 @@ @drop.capture="cancelEditMode" @drop.prevent="dropOnEntry" @click="selectAndEmitEntry($event, entry)" + @paste="addImageFromPaste" >
@@ -372,6 +373,21 @@ export default { this.manageEmbedLayout(); }, + async addImageFromPaste(event) { + const items = (event.clipboardData || event.originalEvent.clipboardData).items; + await Promise.all( + Array.from(items).map(async (item) => { + const isImage = item.type.includes('image') && item.kind === 'file'; + if (isImage) { + const imageFile = item.getAsFile(); + const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name); + this.entry.embeds.push(imageEmbed); + } + }) + ); + this.manageEmbedLayout(); + this.timestampAndUpdate(); + }, convertMarkDownToHtml(text) { let markDownHtml = this.marked.parse(text, { breaks: true, @@ -443,15 +459,27 @@ export default { }, async dropOnEntry(dropEvent) { dropEvent.stopImmediatePropagation(); + const dataTransferFiles = Array.from(dropEvent.dataTransfer.files); - const localImageDropped = dropEvent.dataTransfer.files?.[0]?.type.includes('image'); + const localImageDropped = dataTransferFiles.some((file) => file.type.includes('image')); const snapshotId = dropEvent.dataTransfer.getData('openmct/snapshot/id'); + const domainObjectData = dropEvent.dataTransfer.getData('openmct/domain-object-path'); const imageUrl = dropEvent.dataTransfer.getData('URL'); if (localImageDropped) { - // local image dropped from disk (file) - const imageData = dropEvent.dataTransfer.files[0]; - const imageEmbed = await createNewImageEmbed(imageData, this.openmct, imageData?.name); - this.entry.embeds.push(imageEmbed); + // local image(s) dropped from disk (file) + await Promise.all( + dataTransferFiles.map(async (file) => { + if (file.type.includes('image')) { + const imageData = file; + const imageEmbed = await createNewImageEmbed( + imageData, + this.openmct, + imageData?.name + ); + this.entry.embeds.push(imageEmbed); + } + }) + ); this.manageEmbedLayout(); } else if (imageUrl) { try { @@ -477,11 +505,18 @@ export default { namespace ); saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject); - } else { + } else if (domainObjectData) { // plain domain object - const data = dropEvent.dataTransfer.getData('openmct/domain-object-path'); - const objectPath = JSON.parse(data); + const objectPath = JSON.parse(domainObjectData); await this.addNewEmbed(objectPath); + } else { + this.openmct.notifications.alert( + `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.` + ); + console.warn( + `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.` + ); + return; } this.timestampAndUpdate(); From f90ae48820ab60c1dbe3585a82711069474cc89e Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Thu, 5 Oct 2023 13:47:20 +0200 Subject: [PATCH 02/16] add some nice errors if couch pukes --- .../notebook/components/NotebookEntry.vue | 21 +++++--- .../notebook/utils/notebook-entries.js | 53 ++++++++++--------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 55b703d83d6..7f6f6bfe37a 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -374,12 +374,21 @@ export default { this.manageEmbedLayout(); }, async addImageFromPaste(event) { - const items = (event.clipboardData || event.originalEvent.clipboardData).items; + const clipboardItems = Array.from( + (event.clipboardData || event.originalEvent.clipboardData).items + ); + const hasImage = clipboardItems.some( + (clipboardItem) => clipboardItem.type.includes('image') && clipboardItem.kind === 'file' + ); + // If the clipboard contained an image, prevent the paste event from reaching the textarea. + if (hasImage) { + event.preventDefault(); + } await Promise.all( - Array.from(items).map(async (item) => { - const isImage = item.type.includes('image') && item.kind === 'file'; + Array.from(clipboardItems).map(async (clipboardItem) => { + const isImage = clipboardItem.type.includes('image') && clipboardItem.kind === 'file'; if (isImage) { - const imageFile = item.getAsFile(); + const imageFile = clipboardItem.getAsFile(); const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name); this.entry.embeds.push(imageEmbed); } @@ -490,7 +499,7 @@ export default { this.entry.embeds.push(imageEmbed); this.manageEmbedLayout(); } catch (error) { - this.openmct.notifications.alert(`Unable to add image: ${error.message} `); + this.openmct.notifications.error(`Unable to add image: ${error.message} `); console.error(`Problem embedding remote image`, error); } } else if (snapshotId.length) { @@ -510,7 +519,7 @@ export default { const objectPath = JSON.parse(domainObjectData); await this.addNewEmbed(objectPath); } else { - this.openmct.notifications.alert( + this.openmct.notifications.error( `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.` ); console.warn( diff --git a/src/plugins/notebook/utils/notebook-entries.js b/src/plugins/notebook/utils/notebook-entries.js index 0d9bdd4fa58..df39361d072 100644 --- a/src/plugins/notebook/utils/notebook-entries.js +++ b/src/plugins/notebook/utils/notebook-entries.js @@ -125,30 +125,35 @@ export function createNewImageEmbed(image, openmct, imageName = '') { return new Promise((resolve) => { const reader = new FileReader(); reader.onloadend = async () => { - const base64Data = reader.result; - const blobUrl = URL.createObjectURL(image); - const imageDomainObject = createNotebookImageDomainObject(base64Data); - await saveNotebookImageDomainObject(openmct, imageDomainObject); - const imageThumbnailURL = await getThumbnailURLFromImageUrl(blobUrl); - - const snapshot = { - fullSizeImageObjectIdentifier: imageDomainObject.identifier, - thumbnailImage: { - src: imageThumbnailURL - } - }; - - const embedMetaData = { - bounds: openmct.time.bounds(), - link: null, - objectPath: null, - openmct, - userImage: true, - imageName - }; - - const createdEmbed = await createNewEmbed(embedMetaData, snapshot); - resolve(createdEmbed); + try { + const base64Data = reader.result; + const blobUrl = URL.createObjectURL(image); + const imageDomainObject = createNotebookImageDomainObject(base64Data); + await saveNotebookImageDomainObject(openmct, imageDomainObject); + const imageThumbnailURL = await getThumbnailURLFromImageUrl(blobUrl); + + const snapshot = { + fullSizeImageObjectIdentifier: imageDomainObject.identifier, + thumbnailImage: { + src: imageThumbnailURL + } + }; + + const embedMetaData = { + bounds: openmct.time.bounds(), + link: null, + objectPath: null, + openmct, + userImage: true, + imageName + }; + + const createdEmbed = await createNewEmbed(embedMetaData, snapshot); + resolve(createdEmbed); + } catch (error) { + console.error(`${error.message} - unable to embed image ${imageName}`, error); + openmct.notifications.error(`${error.message} -- unable to embed image ${imageName}`); + } }; reader.readAsDataURL(image); From 07e88ba68edf34aa641485cdb00d4ed2bf89b050 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Thu, 5 Oct 2023 14:41:55 +0200 Subject: [PATCH 03/16] added how to adjust couchdb limits --- src/plugins/persistence/couch/README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/plugins/persistence/couch/README.md b/src/plugins/persistence/couch/README.md index 1e43b5efb49..c01c72d42e0 100644 --- a/src/plugins/persistence/couch/README.md +++ b/src/plugins/persistence/couch/README.md @@ -60,21 +60,21 @@ While we highly recommend using the CouchDB docker-compose installation, it is s 1. Install CouchDB using: `brew install couchdb`. 2. Edit `/usr/local/etc/local.ini` and add the following settings: - ```txt + ```ini [admins] admin = youradminpassword ``` And set the server up for single node: - ```txt + ```ini [couchdb] single_node=true ``` Enable CORS - ```txt + ```ini [chttpd] enable_cors = true [cors] @@ -119,6 +119,18 @@ sh ./setup-couchdb.sh 5. Navigate to 6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`. +## Document Sizes +CouchDB has size limits on both its internal documents, and its httpd interface. If dealing with larger documents in Open MCT (e.g., users adding images to notebook entries), you may to increase this limit. To do this, add the following to the two sections: +```ini + [couchdb] + max_document_size = 4294967296 ; approx 4 GB + + [chttpd] + max_http_request_size = 4294967296 ; approx 4 GB +``` + +If not present, add them under proper sections. The values are in bytes, and can be adjusted to whatever is appropriate for your use case. + # Configuring Open MCT to use CouchDB ## Configuration script @@ -230,4 +242,4 @@ To enable them in Open MCT, we need to configure the plugin `useDesignDocuments` ```js openmct.install(openmct.plugins.CouchDB({url: "http://localhost:5984/openmct", useDesignDocuments: true})); - ``` + ``` \ No newline at end of file From 3717e7a269740fd7f46fba9f55a0a6685ad0f94c Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Thu, 5 Oct 2023 18:48:33 +0200 Subject: [PATCH 04/16] throw error on anntotation change --- src/plugins/notebook/utils/notebook-image.js | 22 ++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/plugins/notebook/utils/notebook-image.js b/src/plugins/notebook/utils/notebook-image.js index a1db1871bd1..224090bc60c 100644 --- a/src/plugins/notebook/utils/notebook-image.js +++ b/src/plugins/notebook/utils/notebook-image.js @@ -54,13 +54,23 @@ export async function saveNotebookImageDomainObject(openmct, object) { await openmct.objects.save(object); } -export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) { - openmct.objects.get(identifier).then((domainObject) => { - const configuration = domainObject.configuration; - configuration.fullSizeImageURL = fullSizeImage.src; - +export async function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) { + const domainObject = await openmct.objects.get(identifier); + const configuration = domainObject.configuration; + configuration.fullSizeImageURL = fullSizeImage.src; + try { + // making a transactions as we can't catch errors on mutations + if (!openmct.objects.isTransactionActive()) { + openmct.objects.startTransaction(); + } openmct.objects.mutate(domainObject, 'configuration', configuration); - }); + const transaction = openmct.objects.getActiveTransaction(); + await transaction.commit(); + openmct.objects.endTransaction(); + } catch (error) { + console.error(`${error.message} -- unable to save image`, error); + openmct.notifications.error(`${error.message} -- unable to save image`); + } } export function updateNamespaceOfDomainObject(object, namespace) { From 9d79092b15ff992feb58fe5fa0a87279a706e696 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 6 Oct 2023 11:59:17 +0200 Subject: [PATCH 05/16] keep blockquotes --- src/plugins/notebook/components/NotebookEntry.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 7f6f6bfe37a..b98351fe258 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -611,7 +611,9 @@ export default { this.editMode = false; const rawEntryValue = $event.target.value; const sanitizeInput = sanitizeHtml(rawEntryValue, { allowedAttributes: [], allowedTags: [] }); - this.entry.text = sanitizeInput; + // change > back to > for markdown to do blockquotes + const restoredQuoteBrackets = sanitizeInput.replace(/>/g, '>'); + this.entry.text = restoredQuoteBrackets; this.timestampAndUpdate(); }, selectAndEmitEntry(event, entry) { From 3114a5c78cf7d480b764e07c6a6722ba9655be23 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 6 Oct 2023 12:10:18 +0200 Subject: [PATCH 06/16] add test for blockquotes --- .../plugins/notebook/notebook.e2e.spec.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index fe684873777..57a128f8e03 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -514,10 +514,23 @@ test.describe('Notebook entry tests', () => { 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); + // Code Blocks + const codeblockTest = '```javascript\nconst foo = "bar";\nconst bar = "foo";\n```'; + await nbUtils.enterTextEntry(page, codeblockTest); const codeBlock = page.locator('code.language-javascript:has-text("const foo = \\"bar\\";")'); await expect(codeBlock).toBeVisible(); + + // Blockquotes + const blockquoteTest = + 'This is a quote by Mark Twain:\n> "The man with a new idea is a crank\n>until the idea succeeds."'; + await nbUtils.enterTextEntry(page, blockquoteTest); + const firstLineOfBlockquoteText = page.locator( + 'blockquote:has-text("The man with a new idea is a crank")' + ); + await expect(firstLineOfBlockquoteText).toBeVisible(); + const secondLineOfBlockquoteText = page.locator( + 'blockquote:has-text("until the idea succeeds")' + ); + await expect(secondLineOfBlockquoteText).toBeVisible(); }); }); From 8ef6f949b51acd40ef1eb40430f676de951a11b3 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 6 Oct 2023 15:48:16 +0200 Subject: [PATCH 07/16] allow multi-file drop of images too --- src/plugins/notebook/components/Notebook.vue | 50 +++++++++++++------ .../notebook/utils/notebook-entries.js | 7 +-- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index eb4edde72f4..c06d3156ac5 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -626,21 +626,35 @@ export default { dropEvent.preventDefault(); dropEvent.stopImmediatePropagation(); - const localImageDropped = dropEvent.dataTransfer.files?.[0]?.type.includes('image'); - const imageUrl = dropEvent.dataTransfer.getData('URL'); + const dataTransferFiles = Array.from(dropEvent.dataTransfer.files); + const localImageDropped = dataTransferFiles.some((file) => file.type.includes('image')); const snapshotId = dropEvent.dataTransfer.getData('openmct/snapshot/id'); + const domainObjectData = dropEvent.dataTransfer.getData('openmct/domain-object-path'); + const imageUrl = dropEvent.dataTransfer.getData('URL'); if (localImageDropped) { - // local image dropped from disk (file) - const imageData = dropEvent.dataTransfer.files[0]; - const imageEmbed = await createNewImageEmbed(imageData, this.openmct, imageData?.name); - this.newEntry(imageEmbed); + // local image(s) dropped from disk (file) + const embeds = []; + await Promise.all( + dataTransferFiles.map(async (file) => { + if (file.type.includes('image')) { + const imageData = file; + const imageEmbed = await createNewImageEmbed( + imageData, + this.openmct, + imageData?.name + ); + embeds.push(imageEmbed); + } + }) + ); + this.newEntry(embeds); } else if (imageUrl) { // remote image dropped (URL) try { const response = await fetch(imageUrl); const imageData = await response.blob(); const imageEmbed = await createNewImageEmbed(imageData, this.openmct); - this.newEntry(imageEmbed); + this.newEntry([imageEmbed]); } catch (error) { this.openmct.notifications.alert(`Unable to add image: ${error.message} `); console.error(`Problem embedding remote image`, error); @@ -648,7 +662,7 @@ export default { } else if (snapshotId.length) { // snapshot object const snapshot = this.snapshotContainer.getSnapshot(snapshotId); - this.newEntry(snapshot.embedObject); + this.newEntry([snapshot.embedObject]); this.snapshotContainer.removeSnapshot(snapshotId); const namespace = this.domainObject.identifier.namespace; @@ -657,10 +671,9 @@ export default { namespace ); saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject); - } else { + } else if (domainObjectData) { // plain domain object - const data = dropEvent.dataTransfer.getData('openmct/domain-object-path'); - const objectPath = JSON.parse(data); + const objectPath = JSON.parse(domainObjectData); const bounds = this.openmct.time.bounds(); const snapshotMeta = { bounds, @@ -669,8 +682,15 @@ export default { openmct: this.openmct }; const embed = await createNewEmbed(snapshotMeta); - - this.newEntry(embed); + this.newEntry([embed]); + } else { + this.openmct.notifications.error( + `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.` + ); + console.warn( + `Unknown object(s) dropped and cannot embed. Try again with an image or domain object.` + ); + return; } }, focusOnEntryId() { @@ -839,12 +859,12 @@ export default { getSelectedSectionId() { return this.selectedSection?.id; }, - async newEntry(embed, event) { + async newEntry(embeds, event) { this.startTransaction(); this.resetSearch(); const notebookStorage = this.createNotebookStorageObject(); this.updateDefaultNotebook(notebookStorage); - const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed); + const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embeds); const element = this.$refs.notebookEntries.querySelector(`#${id}`); const entryAnnotations = this.notebookAnnotations[id] ?? {}; diff --git a/src/plugins/notebook/utils/notebook-entries.js b/src/plugins/notebook/utils/notebook-entries.js index df39361d072..23cebe8cb5c 100644 --- a/src/plugins/notebook/utils/notebook-entries.js +++ b/src/plugins/notebook/utils/notebook-entries.js @@ -207,7 +207,7 @@ export async function addNotebookEntry( openmct, domainObject, notebookStorage, - embed = null, + passedEmbeds = [], entryText = '' ) { if (!openmct || !domainObject || !notebookStorage) { @@ -217,7 +217,8 @@ export async function addNotebookEntry( const date = openmct.time.now(); const configuration = domainObject.configuration; const entries = configuration.entries || {}; - const embeds = embed ? [embed] : []; + // if embeds isn't an array, make it one + const embedsNormalized = Array.isArray(passedEmbeds) ? passedEmbeds : [passedEmbeds]; const id = `entry-${uuid()}`; const [createdBy, createdByRole] = await Promise.all([ @@ -230,7 +231,7 @@ export async function addNotebookEntry( createdBy, createdByRole, text: entryText, - embeds + embeds: embedsNormalized }; const newEntries = addEntryIntoPage(notebookStorage, entries, entry); From 6d34c51e270ea573608ead750ea0e3e7576e5cf9 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 6 Oct 2023 16:03:03 +0200 Subject: [PATCH 08/16] add test for big files --- .../notebook/notebookSnapshots.e2e.spec.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js index e4b80b62eee..c8265d51862 100644 --- a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js @@ -214,4 +214,47 @@ test.describe('Snapshot image tests', () => { // expect one embedded image now as we deleted the other expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1); }); + + test('Get an error notification when dropping unknown file onto notebook entry', async ({ + page + }) => { + // fill Uint8Array array with some garbage data + const garbageData = new Uint8Array(100); + const fileData = Array.from(garbageData); + + const dropTransfer = await page.evaluateHandle((data) => { + const dataTransfer = new DataTransfer(); + const file = new File([new Uint8Array(data)], 'someGarbage.foo', { type: 'unknown/garbage' }); + dataTransfer.items.add(file); + return dataTransfer; + }, fileData); + + await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer }); + + // should have gotten a notification from OpenMCT that we couldn't add it + await expect(page.getByText('Unknown object(s) dropped and cannot embed')).toBeVisible(); + }); + + test('Get an error notification when dropping big files onto notebook entry', async ({ + page + }) => { + const garbageSize = 15 * 1024 * 1024; // 15 megabytes + + await page.addScriptTag({ + // make the garbage client side + content: `window.bigGarbageData = new Uint8Array(${garbageSize})` + }); + + const bigDropTransfer = await page.evaluateHandle(() => { + const dataTransfer = new DataTransfer(); + const file = new File([window.bigGarbageData], 'bigBoy.png', { type: 'image/png' }); + dataTransfer.items.add(file); + return dataTransfer; + }); + + await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: bigDropTransfer }); + + // should have gotten a notification from OpenMCT that we couldn't add it as it's too big + await expect(page.getByText('unable to embed')).toBeVisible(); + }); }); From d544361ea64fa7c0917a4b7e901a2802d7db6c2d Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 6 Oct 2023 16:10:19 +0200 Subject: [PATCH 09/16] spell check --- .cspell.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.cspell.json b/.cspell.json index d09743f3572..3716e48fea4 100644 --- a/.cspell.json +++ b/.cspell.json @@ -483,7 +483,11 @@ "websockets", "swgs", "memlab", - "devmode" + "devmode", + "blockquote", + "blockquotes", + "Blockquote", + "Blockquotes" ], "dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"], "ignorePaths": [ From eeb92ac5b57117a2ecf0a8f6f70dbcc4d35e408a Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 6 Oct 2023 17:39:19 +0200 Subject: [PATCH 10/16] check for null --- src/plugins/notebook/utils/notebook-entries.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/notebook/utils/notebook-entries.js b/src/plugins/notebook/utils/notebook-entries.js index 23cebe8cb5c..ddb40aa25c6 100644 --- a/src/plugins/notebook/utils/notebook-entries.js +++ b/src/plugins/notebook/utils/notebook-entries.js @@ -218,7 +218,8 @@ export async function addNotebookEntry( const configuration = domainObject.configuration; const entries = configuration.entries || {}; // if embeds isn't an array, make it one - const embedsNormalized = Array.isArray(passedEmbeds) ? passedEmbeds : [passedEmbeds]; + const embedsNormalized = + passedEmbeds && !Array.isArray(passedEmbeds) ? [passedEmbeds] : passedEmbeds; const id = `entry-${uuid()}`; const [createdBy, createdByRole] = await Promise.all([ From 9be2086380bbf82205058d14ab11bf5b29f5d414 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 6 Oct 2023 17:47:27 +0200 Subject: [PATCH 11/16] need to ignore console errors --- .../functional/plugins/notebook/notebookSnapshots.e2e.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js index c8265d51862..c591a36c290 100644 --- a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js @@ -218,6 +218,7 @@ test.describe('Snapshot image tests', () => { test('Get an error notification when dropping unknown file onto notebook entry', async ({ page }) => { + test.use({ failOnConsoleError: false }); // fill Uint8Array array with some garbage data const garbageData = new Uint8Array(100); const fileData = Array.from(garbageData); @@ -238,6 +239,7 @@ test.describe('Snapshot image tests', () => { test('Get an error notification when dropping big files onto notebook entry', async ({ page }) => { + test.use({ failOnConsoleError: false }); const garbageSize = 15 * 1024 * 1024; // 15 megabytes await page.addScriptTag({ From f8cf49b9576a9037962678403e107f6ad8eb45db Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 6 Oct 2023 17:56:38 +0200 Subject: [PATCH 12/16] reorder tests so we can ignore console errors --- .../notebook/notebookSnapshots.e2e.spec.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js index c591a36c290..a6bb5efb1bd 100644 --- a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js @@ -214,11 +214,23 @@ test.describe('Snapshot image tests', () => { // expect one embedded image now as we deleted the other expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1); }); +}); + +test.describe('Snapshot image failure tests', () => { + test.use({ failOnConsoleError: false }); + test.beforeEach(async ({ page }) => { + //Navigate to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + // Create Notebook + await createDomainObjectWithDefaults(page, { + type: NOTEBOOK_NAME + }); + }); test('Get an error notification when dropping unknown file onto notebook entry', async ({ page }) => { - test.use({ failOnConsoleError: false }); // fill Uint8Array array with some garbage data const garbageData = new Uint8Array(100); const fileData = Array.from(garbageData); @@ -239,7 +251,6 @@ test.describe('Snapshot image tests', () => { test('Get an error notification when dropping big files onto notebook entry', async ({ page }) => { - test.use({ failOnConsoleError: false }); const garbageSize = 15 * 1024 * 1024; // 15 megabytes await page.addScriptTag({ From 5c24869af3845215de9a9b4a33138e9be919384f Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Tue, 10 Oct 2023 12:02:47 +0200 Subject: [PATCH 13/16] when creating new entry, ready it for editing --- src/plugins/notebook/components/Notebook.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index c06d3156ac5..f5bf5a44373 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -882,6 +882,11 @@ export default { this.filterAndSortEntries(); this.focusEntryId = id; this.selectedEntryId = id; + + // put entry into edit mode + this.$nextTick(() => { + element.dispatchEvent(new Event('click')); + }); }, orientationChange() { this.formatSidebar(); From 9510d922e6e6deaccbc9722ddf23735513e4799d Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Tue, 10 Oct 2023 12:17:42 +0200 Subject: [PATCH 14/16] fix tests and empty embeds --- e2e/helper/notebookUtils.js | 1 - .../plugins/notebook/notebook.e2e.spec.js | 2 +- .../functional/plugins/notebook/tags.e2e.spec.js | 1 - .../contract/notebook.contract.perf.spec.js | 1 - src/plugins/notebook/components/NotebookEntry.vue | 15 +++++++++++++++ 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/e2e/helper/notebookUtils.js b/e2e/helper/notebookUtils.js index ae4e6bd39ec..f51e087190c 100644 --- a/e2e/helper/notebookUtils.js +++ b/e2e/helper/notebookUtils.js @@ -34,7 +34,6 @@ async function enterTextEntry(page, text) { await page.locator(NOTEBOOK_DROP_AREA).click(); // enter text - await page.getByLabel('Notebook Entry Display').last().click(); await page.getByLabel('Notebook Entry Input').last().fill(text); await commitEntry(page); } diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index 57a128f8e03..81e858a88db 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -279,7 +279,7 @@ test.describe('Notebook entry tests', () => { // Click .c-notebook__drag-area await page.locator('.c-notebook__drag-area').click(); - await expect(page.getByLabel('Notebook Entry Display')).toBeVisible(); + await expect(page.getByLabel('Notebook Entry Input')).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 ({ diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js index 89f46c7d3c0..b4c9270b00c 100644 --- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js @@ -150,7 +150,6 @@ test.describe('Tagging in Notebooks @addInit', () => { await createNotebookEntryAndTags(page); await page.locator('text=To start a new entry, click here or drag and drop any object').click(); - 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(); diff --git a/e2e/tests/performance/contract/notebook.contract.perf.spec.js b/e2e/tests/performance/contract/notebook.contract.perf.spec.js index a2db7a0f3c1..a2d811256e6 100644 --- a/e2e/tests/performance/contract/notebook.contract.perf.spec.js +++ b/e2e/tests/performance/contract/notebook.contract.perf.spec.js @@ -131,7 +131,6 @@ test.describe('Performance tests', () => { await page.evaluate(() => window.performance.mark('new-notebook-entry-created')); // Enter Notebook Entry text - 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')); diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index b98351fe258..d7fcc659b37 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -369,6 +369,9 @@ export default { openmct: this.openmct }; const newEmbed = await createNewEmbed(snapshotMeta); + if (!this.entry.embeds) { + this.entry.embeds = []; + } this.entry.embeds.push(newEmbed); this.manageEmbedLayout(); @@ -390,6 +393,9 @@ export default { if (isImage) { const imageFile = clipboardItem.getAsFile(); const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name); + if (!this.entry.embeds) { + this.entry.embeds = []; + } this.entry.embeds.push(imageEmbed); } }) @@ -485,6 +491,9 @@ export default { this.openmct, imageData?.name ); + if (!this.entry.embeds) { + this.entry.embeds = []; + } this.entry.embeds.push(imageEmbed); } }) @@ -496,6 +505,9 @@ export default { const response = await fetch(imageUrl); const imageData = await response.blob(); const imageEmbed = await createNewImageEmbed(imageData, this.openmct); + if (!this.entry.embeds) { + this.entry.embeds = []; + } this.entry.embeds.push(imageEmbed); this.manageEmbedLayout(); } catch (error) { @@ -505,6 +517,9 @@ export default { } else if (snapshotId.length) { // snapshot object const snapshot = this.snapshotContainer.getSnapshot(snapshotId); + if (!this.entry.embeds) { + this.entry.embeds = []; + } this.entry.embeds.push(snapshot.embedObject); this.snapshotContainer.removeSnapshot(snapshotId); From 38ff960d831ce6761df2d3c5d0d6bfb1f0d1e61f Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Tue, 10 Oct 2023 12:36:23 +0200 Subject: [PATCH 15/16] fix tests --- e2e/helper/notebookUtils.js | 1 + .../functional/plugins/notebook/notebookSnapshots.e2e.spec.js | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/helper/notebookUtils.js b/e2e/helper/notebookUtils.js index f51e087190c..75571f8966f 100644 --- a/e2e/helper/notebookUtils.js +++ b/e2e/helper/notebookUtils.js @@ -52,6 +52,7 @@ 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); } /** diff --git a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js index a6bb5efb1bd..f3fa07f58b5 100644 --- a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js @@ -188,12 +188,11 @@ test.describe('Snapshot image tests', () => { }, fileData); await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer }); - + await page.locator('.c-ne__save-button > button').click(); // be sure that entry was created await expect(page.getByText('favicon-96x96.png')).toBeVisible(); await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click(); - // expect large image to be displayed await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible(); From d3832327dd0feeb118ec7689572c6ab7ed603743 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Tue, 10 Oct 2023 20:41:45 +0200 Subject: [PATCH 16/16] found similar issue from notebooks in plots --- src/plugins/plot/MctPlot.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 296cbf77b6e..fe2635d116a 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -1381,6 +1381,9 @@ export default { Object.keys(pointsInBoxBySeries).forEach((seriesKeyString) => { const pointsInBox = pointsInBoxBySeries[seriesKeyString]; if (pointsInBox && pointsInBox.length) { + if (!annotationsBySeries[seriesKeyString]) { + annotationsBySeries[seriesKeyString] = []; + } annotationsBySeries[seriesKeyString].push(...pointsInBox); } });