From b6a2c876f30eab2553ddacd9d4cc9f519d0fa6c6 Mon Sep 17 00:00:00 2001 From: "irina.yugfeld" Date: Sun, 28 Apr 2024 21:30:03 +0300 Subject: [PATCH 1/3] fix: prohibit paste text/html content into email editor, allow paste only plain text --- .../templates/components/email-editor/TextRowContent.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx b/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx index db8c0f0f72a..7274fe8d654 100644 --- a/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx +++ b/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx @@ -270,6 +270,15 @@ export function TextRowContent({ blockIndex }: { blockIndex: number }) { backgroundColor: 'transparent', textAlign: textAlign || TextAlignEnum.LEFT, }} + onPaste={(event) => { + event.preventDefault(); + const pastedData = event?.clipboardData?.getData('text/plain'); + const selection = window.getSelection(); + if (!selection?.rangeCount) return; + selection.deleteFromDocument(); + selection.getRangeAt(0).insertNode(document.createTextNode(pastedData)); + selection.collapseToEnd(); + }} onClick={() => { if (showAutoSuggestions) { resetAutoSuggestions(); From 4ab5e37a2cf50592b65c5c3157810260912d3342 Mon Sep 17 00:00:00 2001 From: "irina.yugfeld" Date: Sun, 19 May 2024 21:58:55 +0300 Subject: [PATCH 2/3] fix: add test, fix bug with paste content via context menu --- apps/web/cypress/global.d.ts | 9 +++++ apps/web/cypress/support/commands.ts | 15 ++++++++ .../create-notification.spec.ts | 38 +++++++++++++++++++ .../email-editor/TextRowContent.tsx | 23 ++++++----- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/apps/web/cypress/global.d.ts b/apps/web/cypress/global.d.ts index 5f6bd26796d..660f7779081 100644 --- a/apps/web/cypress/global.d.ts +++ b/apps/web/cypress/global.d.ts @@ -4,6 +4,10 @@ type IMountType = import('cypress/react').mount; type ICreateNotificationTemplateDto = import('@novu/shared').ICreateNotificationTemplateDto; type FeatureFlagsKeysEnum = import('@novu/shared').FeatureFlagsKeysEnum; type CreateTemplatePayload = import('@novu/testing').CreateTemplatePayload; +type ClipboardData = { + 'text/plain'?: string; + 'text/html'?: string; +}; declare namespace Cypress { interface Chainable { @@ -63,6 +67,11 @@ declare namespace Cypress { */ getClipboardValue(): Chainable; + /** + * Generates paste event on found by given testIdSelector element + */ + paste(testIdSelector: string, clipboardData: ClipboardData): void; + loginWithGitHub(): Chainable; makeBlueprints(): Chainable; diff --git a/apps/web/cypress/support/commands.ts b/apps/web/cypress/support/commands.ts index 48c7b975970..04719bad10c 100644 --- a/apps/web/cypress/support/commands.ts +++ b/apps/web/cypress/support/commands.ts @@ -219,4 +219,19 @@ Cypress.Commands.add('getClipboardValue', () => { .then((clipboard) => clipboard?.readText()); }); +Cypress.Commands.add('paste', (selector: string, data) => { + const pasteEvent = Object.assign(new Event('paste', { bubbles: true, cancelable: true }), { + clipboardData: { + getData(type) { + return data[type]; + }, + }, + }); + cy.getByTestId(selector) + .click() + .then(($el) => { + $el[0].dispatchEvent(pasteEvent); + }); +}); + export {}; diff --git a/apps/web/cypress/tests/notification-editor/create-notification.spec.ts b/apps/web/cypress/tests/notification-editor/create-notification.spec.ts index bc1c379c424..faac7fc4923 100644 --- a/apps/web/cypress/tests/notification-editor/create-notification.spec.ts +++ b/apps/web/cypress/tests/notification-editor/create-notification.spec.ts @@ -468,6 +468,44 @@ describe('Creation functionality', function () { cy.getByTestId('emailSubject').should('have.value', 'this is email subject'); cy.getByTestId('emailPreheader').should('have.value', 'this is email preheader'); }); + + it('should paste only text/plain content into email editor', async function () { + cy.waitLoadTemplatePage(() => { + cy.visit('/workflows/create'); + }); + cy.waitForNetworkIdle(500); + cy.getByTestId('settings-page').click(); + cy.waitForNetworkIdle(500); + cy.getByTestId('title').clear().first().type('Test Notification Title'); + cy.getByTestId('description').type('This is a test description for a test title'); + cy.get('body').click(); + cy.getByTestId('trigger-code-snippet').should('not.exist'); + cy.getByTestId('groupSelector').should('have.value', 'General'); + + addAndEditChannel('email'); + cy.waitForNetworkIdle(500); + + cy.getByTestId('emailSubject').type('this is email subject'); + cy.getByTestId('emailSenderName').type('this is email sender name'); + + const text = '{{firstName}} someone assigned you to {{taskName}}'; + + cy.paste('editable-text-content', { + 'text/plain': text, + 'text/html': `

${text}

`, + }); + + cy.getByTestId('var-label').last().contains('Step Variables').click(); + cy.getByTestId('var-item-firstName-string').contains('firstName'); + cy.getByTestId('var-item-firstName-string').contains('string'); + cy.getByTestId('var-item-taskName-string').contains('taskName'); + cy.getByTestId('var-item-taskName-string').contains('string'); + + cy.getByTestId('editor-mode-switch').find('label').last().click(); + + cy.getByTestId('test-send-email-btn').click(); + cy.get('.mantine-Notification-root').contains('Test sent successfully!'); + }); }); function awaitGetContains(getSelector: string, contains: string) { diff --git a/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx b/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx index 7274fe8d654..541e54f489a 100644 --- a/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx +++ b/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { ClipboardEvent, useEffect, useMemo, useRef, useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { getHotkeyHandler } from '@mantine/hooks'; import { TextAlignEnum } from '@novu/shared'; @@ -157,6 +157,17 @@ export function TextRowContent({ blockIndex }: { blockIndex: number }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [content, text]); + const handlePaste = (event: ClipboardEvent) => { + event.preventDefault(); + const pastedData = event?.clipboardData?.getData('text/plain'); + const selection = window.getSelection(); + if (!selection?.rangeCount) return; + selection.deleteFromDocument(); + selection.getRangeAt(0).insertNode(document.createTextNode(pastedData)); + selection.collapseToEnd(); + methods.setValue(`${stepFormPath}.template.content.${blockIndex}.content`, ref.current?.innerHTML ?? ''); + }; + return (
{showAutoSuggestions && ( @@ -270,15 +281,7 @@ export function TextRowContent({ blockIndex }: { blockIndex: number }) { backgroundColor: 'transparent', textAlign: textAlign || TextAlignEnum.LEFT, }} - onPaste={(event) => { - event.preventDefault(); - const pastedData = event?.clipboardData?.getData('text/plain'); - const selection = window.getSelection(); - if (!selection?.rangeCount) return; - selection.deleteFromDocument(); - selection.getRangeAt(0).insertNode(document.createTextNode(pastedData)); - selection.collapseToEnd(); - }} + onPaste={(event) => handlePaste(event)} onClick={() => { if (showAutoSuggestions) { resetAutoSuggestions(); From 689c162ed3c8069bba05ea1c4df1a19ea959df5c Mon Sep 17 00:00:00 2001 From: "irina.yugfeld" Date: Mon, 20 May 2024 11:02:02 +0300 Subject: [PATCH 3/3] fix: fix problem with paste undo operation --- .../components/email-editor/TextRowContent.tsx | 7 ++----- apps/web/src/utils/paste.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/utils/paste.ts diff --git a/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx b/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx index 541e54f489a..54694459bd5 100644 --- a/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx +++ b/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx @@ -10,6 +10,7 @@ import { useStepFormPath } from '../../hooks/useStepFormPath'; import type { IForm } from '../formTypes'; import { AutoSuggestionsDropdown } from './AutoSuggestionsDropdown'; import { useWorkflowVariables } from '../../../../api/hooks'; +import { paste } from '../../../../utils/paste'; export function TextRowContent({ blockIndex }: { blockIndex: number }) { const methods = useFormContext(); @@ -160,11 +161,7 @@ export function TextRowContent({ blockIndex }: { blockIndex: number }) { const handlePaste = (event: ClipboardEvent) => { event.preventDefault(); const pastedData = event?.clipboardData?.getData('text/plain'); - const selection = window.getSelection(); - if (!selection?.rangeCount) return; - selection.deleteFromDocument(); - selection.getRangeAt(0).insertNode(document.createTextNode(pastedData)); - selection.collapseToEnd(); + paste(pastedData); methods.setValue(`${stepFormPath}.template.content.${blockIndex}.content`, ref.current?.innerHTML ?? ''); }; diff --git a/apps/web/src/utils/paste.ts b/apps/web/src/utils/paste.ts new file mode 100644 index 00000000000..f7c993bc554 --- /dev/null +++ b/apps/web/src/utils/paste.ts @@ -0,0 +1,18 @@ +export function paste(text: string) { + const canPasteByExecCommand = typeof document.execCommand === 'function'; + let isPasteDone = false; + if (canPasteByExecCommand) { + try { + document.execCommand('insertText', false, text); + isPasteDone = true; + } catch (e) {} + } + + if (!isPasteDone) { + const selection = window.getSelection(); + if (!selection?.rangeCount) return; + selection.deleteFromDocument(); + selection.getRangeAt(0).insertNode(document.createTextNode(text)); + selection.collapseToEnd(); + } +}