feat(editor): add "Copy as Markdown" option in context & export menus (#14705)

- Allow users to select text and copy it as Markdown via the context
menu
- Add "Copy as Markdown" under Export menu to copy entire document to
clipboard

Fixes #12983

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added "Copy as Markdown" to the toolbar clipboard More menu for
selected content.
* Added "Copy as Markdown" to the page export menu to copy entire pages
as Markdown.

* **Behavior**
* Export flow now returns success/failure so the UI shows a dedicated
success or error notification for clipboard exports.

* **Localization**
  * Added strings for "Copy as Markdown" and "Copied as Markdown".
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Whitewater <me@waterwater.moe>
Co-authored-by: lawvs <18554747+lawvs@users.noreply.github.com>
This commit is contained in:
Saurabh Pardeshi
2026-04-18 18:09:20 +05:30
committed by GitHub
parent f7d0f1d5ae
commit 0009f91d2a
7 changed files with 254 additions and 23 deletions
@@ -13,6 +13,7 @@ import {
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
@@ -370,6 +371,61 @@ test('should clear selection when switching doc mode', async ({ page }) => {
});
test.describe('Toolbar More Actions', () => {
test('should copy selected text as markdown', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('toolbar-copy-as-markdown');
await selectAllByKeyboard(page);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy as Markdown').click();
const clipboardText = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
expect(clipboardText).toContain('toolbar-copy-as-markdown');
});
test('should preserve heading syntax when copying as markdown from toolbar', async ({
page,
}) => {
await page.keyboard.press('Enter');
await type(page, '# toolbar-heading');
await selectAllByKeyboard(page);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy as Markdown').click();
const clipboardText = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
expect(clipboardText).toContain('# toolbar-heading');
});
test('should preserve bulleted list syntax when copying as markdown from toolbar', async ({
page,
}) => {
await page.keyboard.press('Enter');
await type(page, '- toolbar-list-item');
await selectAllByKeyboard(page);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy as Markdown').click();
const clipboardText = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
expect(clipboardText).toMatch(/^\* toolbar-list-item/m);
});
test('should duplicate block', async ({ page }) => {
await page.keyboard.press('Enter');