Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle paste events for images and text properly #7679

Merged
merged 13 commits into from
Apr 16, 2024
47 changes: 47 additions & 0 deletions e2e/helper/hotkeys/clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/

const isMac = process.platform === 'darwin';
const modifier = isMac ? 'Meta' : 'Control';

/**
* @param {import('@playwright/test').Page} page
*/
async function selectAll(page) {
await page.keyboard.press(`${modifier}+KeyA`);
}

/**
* @param {import('@playwright/test').Page} page
*/
async function copy(page) {
await page.keyboard.press(`${modifier}+KeyC`);
}

/**
* @param {import('@playwright/test').Page} page
*/
async function paste(page) {
await page.keyboard.press(`${modifier}+KeyV`);
}

export { copy, paste, selectAll };
23 changes: 23 additions & 0 deletions e2e/helper/hotkeys/hotkeys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/

export * from './clipboard.js';
21 changes: 18 additions & 3 deletions e2e/helper/notebookUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,26 @@ import { fileURLToPath } from 'url';

/**
* @param {import('@playwright/test').Page} page
* @param {string} text
*/
async function enterTextEntry(page, text) {
// Click the 'Add Notebook Entry' area
await addNotebookEntry(page);
await enterTextInLastEntry(page, text);
await commitEntry(page);
}

/**
* @param {import('@playwright/test').Page} page
*/
async function addNotebookEntry(page) {
await page.locator(NOTEBOOK_DROP_AREA).click();
}

// enter text
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextInLastEntry(page, text) {
await page.getByLabel('Notebook Entry Input').last().fill(text);
await commitEntry(page);
}

/**
Expand Down Expand Up @@ -140,10 +152,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
}

export {
addNotebookEntry,
commitEntry,
createNotebookAndEntry,
createNotebookEntryAndTags,
dragAndDropEmbed,
enterTextEntry,
enterTextInLastEntry,
lockPage,
startAndAddRestrictedNotebookObject
};
50 changes: 50 additions & 0 deletions e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This test suite is dedicated to tests which verify the basic operations surround
import { fileURLToPath } from 'url';

import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
import * as nbUtils from '../../../../helper/notebookUtils.js';
import { expect, streamToString, test } from '../../../../pluginFixtures.js';

Expand Down Expand Up @@ -546,4 +547,53 @@ test.describe('Notebook entry tests', () => {
);
await expect(secondLineOfBlockquoteText).toBeVisible();
});

/**
* Paste into notebook entry tests
*/
test('Can paste text into a notebook entry', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';
const iterations = 20;
const EXPECTED_TEXT = TEST_TEXT.repeat(iterations);

await page.goto(notebookObject.url);

await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
for (let i = 0; i < iterations; i++) {
await paste(page);
}
await nbUtils.commitEntry(page);

await expect(page.locator(`text="${EXPECTED_TEXT}"`)).toBeVisible();
});

test('Prevents pasting text into selected notebook entry if not editing', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';

await page.goto(notebookObject.url);

await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
await paste(page);
await nbUtils.commitEntry(page);

// This should not paste text into the entry
await paste(page);

await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
Comment on lines +596 to +597
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);

});
});
66 changes: 45 additions & 21 deletions src/plugins/notebook/components/NotebookEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
@drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry"
@click="selectAndEmitEntry($event, entry)"
@paste="addImageFromPaste"
@paste="handlePaste"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time-and-creator-and-delete">
Expand Down Expand Up @@ -368,6 +368,28 @@ export default {
}
},
methods: {
handlePaste(event) {
const clipboardItems = Array.from(
(event.clipboardData || event.originalEvent.clipboardData).items
);
const hasClipboardText = clipboardItems.some(
(clipboardItem) => clipboardItem.kind === 'string'
);
const clipboardImages = clipboardItems.filter(
(clipboardItem) => clipboardItem.kind === 'file' && clipboardItem.type.includes('image')
);
const hasClipboardImages = clipboardImages?.length > 0;

if (hasClipboardImages) {
if (hasClipboardText) {
console.warn('Image and text kinds found in paste. Only processing images.');
}

this.addImageFromPaste(clipboardImages, event);
} else if (hasClipboardText) {
this.addTextFromPaste(event);
}
},
async addNewEmbed(objectPath) {
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
Expand All @@ -384,32 +406,34 @@ export default {

this.manageEmbedLayout();
},
async addImageFromPaste(event) {
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) {
addTextFromPaste(event) {
if (!this.editMode) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is easily confused for the API's Edit mode. Can we rename this to make it clear that it refers to the notebook entry being editable?

event.preventDefault();
}
},
async addImageFromPaste(clipboardImages, event) {
event?.preventDefault();
let updated = false;

await Promise.all(
Array.from(clipboardItems).map(async (clipboardItem) => {
const isImage = clipboardItem.type.includes('image') && clipboardItem.kind === 'file';
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);
Array.from(clipboardImages).map(async (clipboardImage) => {
const imageFile = clipboardImage.getAsFile();
const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name);

if (!this.entry.embeds) {
this.entry.embeds = [];
}

this.entry.embeds.push(imageEmbed);

updated = true;
})
);
this.manageEmbedLayout();
this.timestampAndUpdate();

if (updated) {
this.manageEmbedLayout();
this.timestampAndUpdate();
}
},
convertMarkDownToHtml(text = '') {
let markDownHtml = this.marked.parse(text, {
Expand Down
Loading