✨(frontend) added copy-as buttons for HTML and Markdown
Add buttons to copy editor content as HTML or Markdown. Closes #300
This commit is contained in:
@@ -9,6 +9,10 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- ✨(frontend) add buttons to copy document to clipboard as HTML/Markdown #300
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
- ♻️(frontend) More multi theme friendly #325
|
- ♻️(frontend) More multi theme friendly #325
|
||||||
@@ -17,7 +21,7 @@ and this project adheres to
|
|||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
🐛(frontend) invalidate queries after removing user #336
|
- 🐛(frontend) invalidate queries after removing user #336
|
||||||
|
|
||||||
|
|
||||||
## [1.5.1] - 2024-10-10
|
## [1.5.1] - 2024-10-10
|
||||||
|
|||||||
@@ -384,6 +384,81 @@ test.describe('Doc Header', () => {
|
|||||||
}),
|
}),
|
||||||
).toBeHidden();
|
).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(
|
||||||
|
`<h1 data-level="1">Hello World</h1><p></p>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Documents Header mobile', () => {
|
test.describe('Documents Header mobile', () => {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { Button } from '@openfun/cunningham-react';
|
import {
|
||||||
|
Button,
|
||||||
|
VariantType,
|
||||||
|
useToastProvider,
|
||||||
|
} from '@openfun/cunningham-react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Box, DropButton, IconOptions } from '@/components';
|
import { Box, DropButton, IconOptions } from '@/components';
|
||||||
import { useAuthStore } from '@/core';
|
import { useAuthStore } from '@/core';
|
||||||
import { usePanelEditorStore } from '@/features/docs/doc-editor/';
|
import { useDocStore, usePanelEditorStore } from '@/features/docs/doc-editor/';
|
||||||
import {
|
import {
|
||||||
Doc,
|
Doc,
|
||||||
ModalRemoveDoc,
|
ModalRemoveDoc,
|
||||||
@@ -31,6 +35,32 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
|
|||||||
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
||||||
const { isSmallMobile } = useResponsiveStore();
|
const { isSmallMobile } = useResponsiveStore();
|
||||||
const { authenticated } = useAuthStore();
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -125,6 +155,28 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
|
|||||||
{t('Delete document')}
|
{t('Delete document')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsDropOpen(false);
|
||||||
|
void copyCurrentEditorToClipboard('markdown');
|
||||||
|
}}
|
||||||
|
color="primary-text"
|
||||||
|
icon={<span className="material-icons">content_copy</span>}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('Copy as {{format}}', { format: 'Markdown' })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsDropOpen(false);
|
||||||
|
void copyCurrentEditorToClipboard('html');
|
||||||
|
}}
|
||||||
|
color="primary-text"
|
||||||
|
icon={<span className="material-icons">content_copy</span>}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('Copy as {{format}}', { format: 'HTML' })}
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</DropButton>
|
</DropButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user