From bbcb5e0cf1a9f61ea4f1c674b4a7264325da4902 Mon Sep 17 00:00:00 2001 From: rvveber Date: Wed, 16 Oct 2024 15:56:09 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20added=20copy-as=20buttons?= =?UTF-8?q?=20for=20HTML=20and=20Markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add buttons to copy editor content as HTML or Markdown. Closes #300 --- CHANGELOG.md | 6 +- .../__tests__/app-impress/doc-header.spec.ts | 75 +++++++++++++++++++ .../docs/doc-header/components/DocToolBox.tsx | 56 +++++++++++++- 3 files changed, 134 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d0ad5c1..48e8fe0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to ## [Unreleased] +## Added + +- ✨(frontend) add buttons to copy document to clipboard as HTML/Markdown #300 + ## Changed - ♻️(frontend) More multi theme friendly #325 @@ -17,7 +21,7 @@ and this project adheres to ## Fixed -🐛(frontend) invalidate queries after removing user #336 +- 🐛(frontend) invalidate queries after removing user #336 ## [1.5.1] - 2024-10-10 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts index d02168de..6c9b59d1 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts @@ -384,6 +384,81 @@ test.describe('Doc Header', () => { }), ).toBeHidden(); }); + + test('It checks the copy as Markdown button', async ({ + page, + browserName, + }) => { + // eslint-disable-next-line playwright/no-skipped-test + test.skip( + browserName === 'webkit', + 'navigator.clipboard is not working with webkit and playwright', + ); + + // create page and navigate to it + await page + .getByRole('button', { + name: 'Create a new document', + }) + .click(); + + // Add dummy content to the doc + const editor = page.locator('.ProseMirror'); + const docFirstBlock = editor.locator('.bn-block-content').first(); + await docFirstBlock.click(); + await page.keyboard.type('# Hello World', { delay: 100 }); + const docFirstBlockContent = docFirstBlock.locator('h1'); + await expect(docFirstBlockContent).toHaveText('Hello World'); + + // Copy content to clipboard + await page.getByLabel('Open the document options').click(); + await page.getByRole('button', { name: 'Copy as Markdown' }).click(); + await expect(page.getByText('Copied to clipboard')).toBeVisible(); + + // Test that clipboard is in Markdown format + const handle = await page.evaluateHandle(() => + navigator.clipboard.readText(), + ); + const clipboardContent = await handle.jsonValue(); + expect(clipboardContent.trim()).toBe('# Hello World'); + }); + + test('It checks the copy as HTML button', async ({ page, browserName }) => { + // eslint-disable-next-line playwright/no-skipped-test + test.skip( + browserName === 'webkit', + 'navigator.clipboard is not working with webkit and playwright', + ); + + // create page and navigate to it + await page + .getByRole('button', { + name: 'Create a new document', + }) + .click(); + + // Add dummy content to the doc + const editor = page.locator('.ProseMirror'); + const docFirstBlock = editor.locator('.bn-block-content').first(); + await docFirstBlock.click(); + await page.keyboard.type('# Hello World', { delay: 100 }); + const docFirstBlockContent = docFirstBlock.locator('h1'); + await expect(docFirstBlockContent).toHaveText('Hello World'); + + // Copy content to clipboard + await page.getByLabel('Open the document options').click(); + await page.getByRole('button', { name: 'Copy as HTML' }).click(); + await expect(page.getByText('Copied to clipboard')).toBeVisible(); + + // Test that clipboard is in HTML format + const handle = await page.evaluateHandle(() => + navigator.clipboard.readText(), + ); + const clipboardContent = await handle.jsonValue(); + expect(clipboardContent.trim()).toBe( + `

Hello World

`, + ); + }); }); test.describe('Documents Header mobile', () => { diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx index d362efa3..2ce583e9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx @@ -1,10 +1,14 @@ -import { Button } from '@openfun/cunningham-react'; +import { + Button, + VariantType, + useToastProvider, +} from '@openfun/cunningham-react'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, DropButton, IconOptions } from '@/components'; import { useAuthStore } from '@/core'; -import { usePanelEditorStore } from '@/features/docs/doc-editor/'; +import { useDocStore, usePanelEditorStore } from '@/features/docs/doc-editor/'; import { Doc, ModalRemoveDoc, @@ -31,6 +35,32 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => { const [isModalVersionOpen, setIsModalVersionOpen] = useState(false); const { isSmallMobile } = useResponsiveStore(); const { authenticated } = useAuthStore(); + const { docsStore } = useDocStore(); + const { toast } = useToastProvider(); + + const copyCurrentEditorToClipboard = async ( + asFormat: 'html' | 'markdown', + ) => { + const editor = docsStore[doc.id]?.editor; + if (!editor) { + toast(t('Editor unavailable'), VariantType.ERROR, { duration: 3000 }); + return; + } + + try { + const editorContentFormatted = + asFormat === 'html' + ? await editor.blocksToHTMLLossy() + : await editor.blocksToMarkdownLossy(); + await navigator.clipboard.writeText(editorContentFormatted); + toast(t('Copied to clipboard'), VariantType.SUCCESS, { duration: 3000 }); + } catch (error) { + console.error(error); + toast(t('Failed to copy to clipboard'), VariantType.ERROR, { + duration: 3000, + }); + } + }; return ( { {t('Delete document')} )} + +