Skip to content

Commit

Permalink
Embedding images in notebook entries (#7048)
Browse files Browse the repository at this point in the history
* initial drag drop, wip

* images work as snapshots, but need to disable navigate to actions

* embed image name

* works now with images, need to be refactor so can duplicate code for entries too

* works dropping on entries too

* handle remote images too

* add e2e test

* spelling

* address most PR comments
  • Loading branch information
scottbell committed Sep 18, 2023
1 parent c7b5ecb commit 541a022
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/

const fs = require('fs').promises;
const { test, expect } = require('../../../../pluginFixtures');
// const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
// const nbUtils = require('../../../../helper/notebookUtils');
const { createDomainObjectWithDefaults } = require('../../../../appActions');

const NOTEBOOK_NAME = 'Notebook';

test.describe('Snapshot Menu tests', () => {
test.fixme(
Expand Down Expand Up @@ -161,3 +163,57 @@ test.describe('Snapshot Container tests', () => {
}
);
});

test.describe('Snapshot image tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });

// Create Notebook
await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});

test('Can drop an image onto a notebook and create a new entry', async ({ page }) => {
const imageData = await fs.readFile('src/images/favicons/favicon-96x96.png');
const imageArray = new Uint8Array(imageData);
const fileData = Array.from(imageArray);

const dropTransfer = await page.evaluateHandle((data) => {
const dataTransfer = new DataTransfer();
const file = new File([new Uint8Array(data)], 'favicon-96x96.png', { type: 'image/png' });
dataTransfer.items.add(file);
return dataTransfer;
}, fileData);

await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });

// 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
await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();

await page.getByLabel('Close').click();

// drop another image onto the entry
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });

// expect two embedded images now
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(2);

await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();

await page.getByRole('menuitem', { name: /Remove This Embed/ }).click();

await page.getByRole('button', { name: 'Ok', exact: true }).click();

// expect one embedded image now as we deleted the other
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1);
});
});
59 changes: 39 additions & 20 deletions src/plugins/notebook/components/Notebook.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ import { isNotebookViewType, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-consta
import {
addNotebookEntry,
createNewEmbed,
createNewImageEmbed,
getEntryPosById,
getNotebookEntries,
mutateObject,
Expand Down Expand Up @@ -615,12 +616,31 @@ export default {
this.openmct.editor.cancel();
}
},
async dropOnEntry(event) {
event.preventDefault();
event.stopImmediatePropagation();
const snapshotId = event.dataTransfer.getData('openmct/snapshot/id');
if (snapshotId.length) {
async dropOnEntry(dropEvent) {
dropEvent.preventDefault();
dropEvent.stopImmediatePropagation();
const localImageDropped = dropEvent.dataTransfer.files?.[0]?.type.includes('image');
const imageUrl = dropEvent.dataTransfer.getData('URL');
const snapshotId = dropEvent.dataTransfer.getData('openmct/snapshot/id');
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);
} 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);
} catch (error) {
this.openmct.notifications.alert(`Unable to add image: ${error.message} `);
console.error(`Problem embedding remote image`, error);
}
} else if (snapshotId.length) {
// snapshot object
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.newEntry(snapshot.embedObject);
this.snapshotContainer.removeSnapshot(snapshotId);
Expand All @@ -631,22 +651,21 @@ export default {
namespace
);
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
} else {
// plain domain object
const data = dropEvent.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
};
const embed = await createNewEmbed(snapshotMeta);
return;
this.newEntry(embed);
}
const data = event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
};
const embed = await createNewEmbed(snapshotMeta);
this.newEntry(embed);
},
focusOnEntryId() {
if (!this.focusEntryId) {
Expand Down
62 changes: 36 additions & 26 deletions src/plugins/notebook/components/NotebookEmbed.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@
@mouseleave="hideToolTip"
>
<div v-if="embed.snapshot" class="c-ne__embed__snap-thumb" @click="openSnapshot()">
<img :src="thumbnailImage" />
<img :src="thumbnailImage" :alt="`${embed.name} thumbnail`" />
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
<a class="c-ne__embed__link" :class="embed.cssClass" @click="navigateToItemInTime">{{
embed.name
}}</a>
<a class="c-ne__embed__link" :class="embed.cssClass" @click="navigateToItemInTime">
{{ embed.name }}
</a>
<button
class="c-ne__embed__actions c-icon-button icon-3-dots"
title="More options"
Expand Down Expand Up @@ -144,31 +144,33 @@ export default {
this.menuActions.splice(0, this.menuActions.length, viewSnapshot);
}
const navigateToItem = {
id: 'navigateToItem',
cssClass: this.embed.cssClass,
name: 'Navigate to Item',
description: 'Navigate to the item with the current time settings.',
onItemClicked: () => this.navigateToItem()
};
if (this.embed.domainObject) {
const navigateToItem = {
id: 'navigateToItem',
cssClass: this.embed.cssClass,
name: 'Navigate to Item',
description: 'Navigate to the item with the current time settings.',
onItemClicked: () => this.navigateToItem()
};
const navigateToItemInTime = {
id: 'navigateToItemInTime',
cssClass: this.embed.cssClass,
name: 'Navigate to Item in Time',
description: 'Navigate to the item in its time frame when captured.',
onItemClicked: () => this.navigateToItemInTime()
};
const navigateToItemInTime = {
id: 'navigateToItemInTime',
cssClass: this.embed.cssClass,
name: 'Navigate to Item in Time',
description: 'Navigate to the item in its time frame when captured.',
onItemClicked: () => this.navigateToItemInTime()
};
const quickView = {
id: 'quickView',
cssClass: 'icon-eye-open',
name: 'Quick View',
description: 'Full screen overlay view of the item.',
onItemClicked: () => this.previewEmbed()
};
const quickView = {
id: 'quickView',
cssClass: 'icon-eye-open',
name: 'Quick View',
description: 'Full screen overlay view of the item.',
onItemClicked: () => this.previewEmbed()
};
this.menuActions.push(...[quickView, navigateToItem, navigateToItemInTime]);
this.menuActions.push(...[quickView, navigateToItem, navigateToItemInTime]);
}
if (!this.isLocked) {
const removeEmbed = {
Expand All @@ -183,6 +185,9 @@ export default {
}
},
async setEmbedObjectPath() {
if (!this.embed.domainObject) {
return;
}
this.objectPath = await this.openmct.objects.getOriginalPath(
this.embed.domainObject.identifier
);
Expand Down Expand Up @@ -260,6 +265,11 @@ export default {
this.openmct.router.navigate(url);
},
navigateToItemInTime() {
if (!this.embed.historicLink) {
// no historic link available
return;
}
const hash = this.embed.historicLink;
const bounds = this.openmct.time.bounds();
Expand Down
36 changes: 29 additions & 7 deletions src/plugins/notebook/components/NotebookEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ import Moment from 'moment';
import sanitizeHtml from 'sanitize-html';
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
import { createNewEmbed, selectEntry } from '../utils/notebook-entries';
import { createNewEmbed, createNewImageEmbed, selectEntry } from '../utils/notebook-entries';
import {
saveNotebookImageDomainObject,
updateNamespaceOfDomainObject
Expand Down Expand Up @@ -359,11 +359,32 @@ export default {
this.enableEmbedsWrapperScroll = embedsTotalWidth > embedsWrapperLength;
}
},
async dropOnEntry($event) {
$event.stopImmediatePropagation();
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
if (snapshotId.length) {
async dropOnEntry(dropEvent) {
dropEvent.stopImmediatePropagation();
const localImageDropped = dropEvent.dataTransfer.files?.[0]?.type.includes('image');
const snapshotId = dropEvent.dataTransfer.getData('openmct/snapshot/id');
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);
this.manageEmbedLayout();
} else if (imageUrl) {
try {
// remote image dropped (URL)
const response = await fetch(imageUrl);
const imageData = await response.blob();
const imageEmbed = await createNewImageEmbed(imageData, this.openmct);
this.entry.embeds.push(imageEmbed);
this.manageEmbedLayout();
} catch (error) {
this.openmct.notifications.alert(`Unable to add image: ${error.message} `);
console.error(`Problem embedding remote image`, error);
}
} else if (snapshotId.length) {
// snapshot object
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.entry.embeds.push(snapshot.embedObject);
this.snapshotContainer.removeSnapshot(snapshotId);
Expand All @@ -375,7 +396,8 @@ export default {
);
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
} else {
const data = $event.dataTransfer.getData('openmct/domain-object-path');
// plain domain object
const data = dropEvent.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
await this.addNewEmbed(objectPath);
}
Expand Down

0 comments on commit 541a022

Please sign in to comment.