feat(editor): add "Copy as Markdown" option in context & export menus#14705
feat(editor): add "Copy as Markdown" option in context & export menus#14705asaurabhprsas wants to merge 9 commits intotoeverything:canaryfrom
Conversation
- 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 toeverything#12983
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds "Copy as Markdown" actions to the editor toolbar and export menu that draft selected models or the full document, convert to Markdown, write the Markdown to the clipboard, and show a localized toast notification. Changes
Sequence DiagramsequenceDiagram
actor User
participant UI as rgba(54,162,235,0.5) Toolbar / Export UI
participant Editor as rgba(75,192,192,0.5) Editor Core
participant Draft as rgba(255,99,132,0.5) Drafting Command
participant Transformer as rgba(153,102,255,0.5) Markdown Transformer
participant Clipboard as rgba(255,159,64,0.5) Clipboard API
participant Toast as rgba(255,205,86,0.5) Toast Notification
User->>UI: Trigger "Copy as Markdown"
UI->>Editor: getSelectedModelsCommand()
Editor-->>UI: selected models
UI->>Draft: draftSelectedModelsCommand(selected models)
Draft-->>UI: drafted models (Slice)
UI->>Transformer: getContentFromSlice(slice, "markdown")
Transformer-->>UI: markdown text
UI->>Clipboard: navigator.clipboard.writeText(markdown)
Clipboard-->>UI: success / failure
UI->>Toast: show localized toast (copied or error)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/frontend/core/src/blocksuite/view-extensions/editor-config/toolbar/index.ts (1)
123-133:⚠️ Potential issue | 🟡 Minor
splice(copyIndex + 1, ...)is inserting new items in reverse order.Because all three inserts target the same index, the last inserted item appears first. If intended order is copy-link → copy-as-image → copy-as-markdown, this currently won’t hold.
Suggested fix
- clipboardGroup.items.splice( - copyIndex + 1, - 0, - createCopyLinkToBlockMenuItem(framework) - ); - - clipboardGroup.items.splice( - copyIndex + 1, - 0, - createCopyAsPngMenuItem(framework) - ); - - clipboardGroup.items.splice( - copyIndex + 1, - 0, - createCopyAsMarkdownMenuItem(framework) - ); + clipboardGroup.items.splice( + copyIndex + 1, + 0, + createCopyLinkToBlockMenuItem(framework), + createCopyAsPngMenuItem(framework), + createCopyAsMarkdownMenuItem(framework) + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/core/src/blocksuite/view-extensions/editor-config/toolbar/index.ts` around lines 123 - 133, Multiple splice calls at the same index cause the items to land in reverse; replace the successive splices with a single splice that inserts all menu items in the desired order (e.g., copy-link, copy-as-image, copy-as-markdown) so use clipboardGroup.items.splice(copyIndex + 1, 0, createCopyLinkMenuItem(framework), createCopyAsPngMenuItem(framework), createCopyAsMarkdownMenuItem(framework)) or otherwise insert them in increasing index order; update references to createCopyAsPngMenuItem, createCopyAsMarkdownMenuItem (and createCopyLinkMenuItem) accordingly.
🧹 Nitpick comments (2)
packages/frontend/core/src/blocksuite/view-extensions/editor-config/toolbar/index.ts (1)
223-270: Copy-as-markdown logic is duplicated in two menu systems.Both V1 and V2 paths implement nearly the same command → draft → slice → markdown → clipboard pipeline. Extracting a shared helper will prevent behavior drift and simplify future fixes.
Refactor sketch
+async function copySelectionAsMarkdown(std: MenuContext['std']) { + const [ok, ctx] = std.command + .chain() + .pipe(getSelectedModelsCommand) + .pipe(draftSelectedModelsCommand) + .run(); + const draftedModels = ctx.draftedModels; + if (!ok || !draftedModels) return false; + const models = await draftedModels; + if (!models.length) return false; + const slice = Slice.fromModels(std.store, models); + const markdown = await getContentFromSlice(std.host, slice, 'markdown'); + if (!markdown) return false; + await navigator.clipboard.writeText(markdown); + toast(std.host, I18n['com.affine.export.copied-as-markdown']()); + return true; +}Also applies to: 291-330
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/core/src/blocksuite/view-extensions/editor-config/toolbar/index.ts` around lines 223 - 270, The copy-as-markdown flow is duplicated between V1 and V2 menu items; extract the shared pipeline from createCopyAsMarkdownMenuItem into a single helper (e.g., exportAsMarkdownAndCopy) that accepts the command std/context and selected models, uses getSelectedModelsCommand → draftSelectedModelsCommand flow, awaits draftedModels, builds Slice.fromModels(std.store, models), calls getContentFromSlice(std.host, slice, 'markdown'), writes to navigator.clipboard and calls toast; update createCopyAsMarkdownMenuItem and the other duplicated menu factory to call this helper, preserve error handling (.catch(console.error)) and ctx.close() behavior, and keep existing identifiers (createCopyAsMarkdownMenuItem, getSelectedModelsCommand, draftSelectedModelsCommand, draftedModels, Slice.fromModels, getContentFromSlice) so behavior remains identical.packages/frontend/core/src/components/hooks/affine/use-export-page.ts (1)
149-162: Extract shared transformer construction to avoid drift.The transformer setup in Line 149-162 duplicates the one in
exportDoc(Line 72-85). A shared factory would reduce maintenance risk.Refactor sketch
+function createDocTransformer(doc: Store) { + return new Transformer({ + schema: getAFFiNEWorkspaceSchema(), + blobCRUD: doc.workspace.blobSync, + docCRUD: { + create: (id: string) => doc.workspace.createDoc(id).getStore({ id }), + get: (id: string) => doc.workspace.getDoc(id)?.getStore({ id }) ?? null, + delete: (id: string) => doc.workspace.removeDoc(id), + }, + middlewares: [ + docLinkBaseURLMiddleware(doc.workspace.id), + titleMiddleware(doc.workspace.meta.docMetas), + embedSyncedDocMiddleware('content'), + ], + }); +} @@ - const transformer = new Transformer({ ... }); + const transformer = createDocTransformer(doc);Also applies to: 72-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts` around lines 149 - 162, The Transformer construction is duplicated (one at the top: the const transformer = new Transformer({...}) using getAFFiNEWorkspaceSchema(), doc.workspace.blobSync and middlewares including docLinkBaseURLMiddleware, titleMiddleware, embedSyncedDocMiddleware, and another similar block in exportDoc); extract this into a shared factory function (e.g., createWorkspaceTransformer) that accepts the workspace or doc and returns a new Transformer configured the same way, then replace both direct new Transformer(...) usages with calls to that factory (update places referencing transformer and exportDoc to use createWorkspaceTransformer(doc.workspace) or similar). Ensure the factory uses the same schema function getAFFiNEWorkspaceSchema(), the same blobCRUD/docCRUD closures (create/get/delete using workspace.createDoc/getDoc/removeDoc), and the same middlewares so behavior is identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts`:
- Around line 145-148: The copyAsMarkdown function currently returns early when
std is missing, causing callers to always show a "Copied as Markdown" toast;
change copyAsMarkdown (in use-export-page.ts) to return a boolean indicating
success (false when std is falsy or when copy fails, true on success) and update
any callers that show the toast (the code path that displays "Copied as
Markdown") to check the returned boolean and only show the success toast when
true (also handle false by optionally showing an error toast or no toast).
Ensure you reference the copyAsMarkdown function and the toast/display logic so
the early-return case no longer produces a false-success message.
---
Outside diff comments:
In
`@packages/frontend/core/src/blocksuite/view-extensions/editor-config/toolbar/index.ts`:
- Around line 123-133: Multiple splice calls at the same index cause the items
to land in reverse; replace the successive splices with a single splice that
inserts all menu items in the desired order (e.g., copy-link, copy-as-image,
copy-as-markdown) so use clipboardGroup.items.splice(copyIndex + 1, 0,
createCopyLinkMenuItem(framework), createCopyAsPngMenuItem(framework),
createCopyAsMarkdownMenuItem(framework)) or otherwise insert them in increasing
index order; update references to createCopyAsPngMenuItem,
createCopyAsMarkdownMenuItem (and createCopyLinkMenuItem) accordingly.
---
Nitpick comments:
In
`@packages/frontend/core/src/blocksuite/view-extensions/editor-config/toolbar/index.ts`:
- Around line 223-270: The copy-as-markdown flow is duplicated between V1 and V2
menu items; extract the shared pipeline from createCopyAsMarkdownMenuItem into a
single helper (e.g., exportAsMarkdownAndCopy) that accepts the command
std/context and selected models, uses getSelectedModelsCommand →
draftSelectedModelsCommand flow, awaits draftedModels, builds
Slice.fromModels(std.store, models), calls getContentFromSlice(std.host, slice,
'markdown'), writes to navigator.clipboard and calls toast; update
createCopyAsMarkdownMenuItem and the other duplicated menu factory to call this
helper, preserve error handling (.catch(console.error)) and ctx.close()
behavior, and keep existing identifiers (createCopyAsMarkdownMenuItem,
getSelectedModelsCommand, draftSelectedModelsCommand, draftedModels,
Slice.fromModels, getContentFromSlice) so behavior remains identical.
In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts`:
- Around line 149-162: The Transformer construction is duplicated (one at the
top: the const transformer = new Transformer({...}) using
getAFFiNEWorkspaceSchema(), doc.workspace.blobSync and middlewares including
docLinkBaseURLMiddleware, titleMiddleware, embedSyncedDocMiddleware, and another
similar block in exportDoc); extract this into a shared factory function (e.g.,
createWorkspaceTransformer) that accepts the workspace or doc and returns a new
Transformer configured the same way, then replace both direct new
Transformer(...) usages with calls to that factory (update places referencing
transformer and exportDoc to use createWorkspaceTransformer(doc.workspace) or
similar). Ensure the factory uses the same schema function
getAFFiNEWorkspaceSchema(), the same blobCRUD/docCRUD closures
(create/get/delete using workspace.createDoc/getDoc/removeDoc), and the same
middlewares so behavior is identical.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ed0d4aba-1b6d-418f-85af-30c6d2cedb73
📒 Files selected for processing (4)
packages/frontend/core/src/blocksuite/view-extensions/editor-config/toolbar/index.tspackages/frontend/core/src/components/hooks/affine/use-export-page.tspackages/frontend/core/src/components/page-list/operation-menu-items/export.tsxpackages/frontend/i18n/src/resources/en.json
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/frontend/core/src/components/hooks/affine/use-export-page.ts (2)
260-265: Unusedsuccessvariable for non-copy-markdown exports.The
successvariable is captured at Line 244 but ignored for non-'copy-markdown'export types (Lines 260-265). The success notification is shown unconditionally. This is currently safe since other export functions throw on failure (caught by the outercatch), but it's inconsistent with the'copy-markdown'handling pattern.Consider either using the
successvalue consistently for all types, or document why this asymmetry is intentional.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts` around lines 260 - 265, The code captures a local variable named success (around Line 244) but then ignores it for non-'copy-markdown' export flows, while showing notify.success unconditionally; update the export branches inside the export handler in use-export-page (the functions that perform exports for types other than 'copy-markdown') to return/assign a boolean result into success and then only call notify.success when success is truthy, or explicitly document/annotate the asymmetry by adding a clear comment and ESLint-disable if you intentionally ignore success; reference the existing success variable and the 'copy-markdown' branch to mirror its handling so the notify.success call at the end uses that same success value consistently.
145-182: Consider extracting shared Transformer creation logic.Lines 151-164 duplicate the Transformer configuration from
exportDoc(lines 72-85). Extracting a helper function would reduce duplication and ensure consistency.♻️ Proposed refactor
+function createDocTransformer(doc: Store) { + return new Transformer({ + schema: getAFFiNEWorkspaceSchema(), + blobCRUD: doc.workspace.blobSync, + docCRUD: { + create: (id: string) => doc.workspace.createDoc(id).getStore({ id }), + get: (id: string) => doc.workspace.getDoc(id)?.getStore({ id }) ?? null, + delete: (id: string) => doc.workspace.removeDoc(id), + }, + middlewares: [ + docLinkBaseURLMiddleware(doc.workspace.id), + titleMiddleware(doc.workspace.meta.docMetas), + embedSyncedDocMiddleware('content'), + ], + }); +} async function exportDoc( doc: Store, std: BlockStdScope, config: AdapterConfig ) { - const transformer = new Transformer({ - schema: getAFFiNEWorkspaceSchema(), - blobCRUD: doc.workspace.blobSync, - docCRUD: { - create: (id: string) => doc.workspace.createDoc(id).getStore({ id }), - get: (id: string) => doc.workspace.getDoc(id)?.getStore({ id }) ?? null, - delete: (id: string) => doc.workspace.removeDoc(id), - }, - middlewares: [ - docLinkBaseURLMiddleware(doc.workspace.id), - titleMiddleware(doc.workspace.meta.docMetas), - embedSyncedDocMiddleware('content'), - ], - }); + const transformer = createDocTransformer(doc); // ... rest of function } async function copyAsMarkdown(doc: Store, std?: BlockStdScope) { // ... try { - const transformer = new Transformer({ - schema: getAFFiNEWorkspaceSchema(), - // ... same config - }); + const transformer = createDocTransformer(doc); // ... rest of function } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts` around lines 145 - 182, The Transformer configuration in copyAsMarkdown duplicates the one used in exportDoc; extract that shared logic into a helper (e.g., createTransformer or getTransformer) that accepts the Store (doc) and optional std/BlockStdScope and returns a configured Transformer with schema, blobCRUD, docCRUD and the middlewares (docLinkBaseURLMiddleware, titleMiddleware, embedSyncedDocMiddleware). Replace the inline new Transformer({...}) in both copyAsMarkdown and exportDoc to call this helper so both use the same construction and avoid duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts`:
- Around line 260-265: The code captures a local variable named success (around
Line 244) but then ignores it for non-'copy-markdown' export flows, while
showing notify.success unconditionally; update the export branches inside the
export handler in use-export-page (the functions that perform exports for types
other than 'copy-markdown') to return/assign a boolean result into success and
then only call notify.success when success is truthy, or explicitly
document/annotate the asymmetry by adding a clear comment and ESLint-disable if
you intentionally ignore success; reference the existing success variable and
the 'copy-markdown' branch to mirror its handling so the notify.success call at
the end uses that same success value consistently.
- Around line 145-182: The Transformer configuration in copyAsMarkdown
duplicates the one used in exportDoc; extract that shared logic into a helper
(e.g., createTransformer or getTransformer) that accepts the Store (doc) and
optional std/BlockStdScope and returns a configured Transformer with schema,
blobCRUD, docCRUD and the middlewares (docLinkBaseURLMiddleware,
titleMiddleware, embedSyncedDocMiddleware). Replace the inline new
Transformer({...}) in both copyAsMarkdown and exportDoc to call this helper so
both use the same construction and avoid duplication.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7dcb40fc-fda4-4d56-917d-d5dac22370e2
📒 Files selected for processing (1)
packages/frontend/core/src/components/hooks/affine/use-export-page.ts
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/frontend/core/src/components/hooks/affine/use-export-page.ts (1)
95-97:⚠️ Potential issue | 🟠 MajorPropagate adapter no-op as failure to avoid false success toasts.
exportDoccan exit without exporting anything, but HTML/Markdown paths still returntrue, so the UI can still show success when no file was produced.Suggested fix
-async function exportDoc( +async function exportDoc( doc: Store, std: BlockStdScope, config: AdapterConfig -) { +): Promise<boolean> { @@ if (!result || (!result.file && !result.assetsIds.length)) { - return; + return false; } @@ download(downloadBlob, name); + return true; } -async function exportToHtml(doc: Store, std?: BlockStdScope) { +async function exportToHtml(doc: Store, std?: BlockStdScope): Promise<boolean> { if (!std) { // If std is not provided, we use the default export method await HtmlTransformer.exportDoc(doc); + return true; } else { - await exportDoc(doc, std, { + return await exportDoc(doc, std, { identifier: HtmlAdapterFactoryIdentifier, @@ -async function exportToMarkdown(doc: Store, std?: BlockStdScope) { +async function exportToMarkdown( + doc: Store, + std?: BlockStdScope +): Promise<boolean> { if (!std) { // If std is not provided, we use the default export method await MarkdownTransformer.exportDoc(doc); + return true; } else { - await exportDoc(doc, std, { + return await exportDoc(doc, std, { identifier: MarkdownAdapterFactoryIdentifier, @@ case 'html': - await exportToHtml(page, editorRoot?.std); - return true; + return await exportToHtml(page, editorRoot?.std); case 'markdown': - await exportToMarkdown(page, editorRoot?.std); - return true; + return await exportToMarkdown(page, editorRoot?.std);Also applies to: 186-189
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts` around lines 95 - 97, The export flow currently treats adapter no-ops as success because exportDoc (in use-export-page.ts) can return undefined/true for HTML/Markdown even when nothing was produced; change the early-exit checks that read "if (!result || (!result.file && !result.assetsIds.length)) { return; }" to explicitly return false so the caller receives a failure signal, and apply the same fix to the analogous check around the later block (lines handling result.file and result.assetsIds). Ensure the function signature and callers accept/propagate the boolean return so UI success toasts only show when a real file or assets were produced.
🧹 Nitpick comments (1)
packages/frontend/core/src/components/hooks/affine/use-export-page.ts (1)
166-167: Prefer the shared clipboard utility instead of directnavigator.clipboard.Using the existing clipboard helper keeps feature detection/fallback behavior consistent with the rest of the app.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts` around lines 166 - 167, Replace the direct call to navigator.clipboard.writeText(result.file) in the useExportPage hook with the app's shared clipboard utility (e.g., copyToClipboard or clipboard.writeText helper); import the shared helper at the top of use-export-page.ts, call it with result.file, and keep the same return true flow so feature detection/fallback behavior is consistent with the rest of the app.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts`:
- Around line 203-205: The current export path returns true even when editorRoot
is null because the optional chaining makes the call a no-op; update the export
logic in use-export-page so it explicitly checks editorRoot (and/or
editorRoot.std) before calling ExportManager.exportPng(): if editorRoot is
missing, return false (or throw/handle error) and only await
editorRoot.std.get(ExportManager).exportPng() when the context exists, ensuring
the function does not report success when export could not run.
---
Outside diff comments:
In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts`:
- Around line 95-97: The export flow currently treats adapter no-ops as success
because exportDoc (in use-export-page.ts) can return undefined/true for
HTML/Markdown even when nothing was produced; change the early-exit checks that
read "if (!result || (!result.file && !result.assetsIds.length)) { return; }" to
explicitly return false so the caller receives a failure signal, and apply the
same fix to the analogous check around the later block (lines handling
result.file and result.assetsIds). Ensure the function signature and callers
accept/propagate the boolean return so UI success toasts only show when a real
file or assets were produced.
---
Nitpick comments:
In `@packages/frontend/core/src/components/hooks/affine/use-export-page.ts`:
- Around line 166-167: Replace the direct call to
navigator.clipboard.writeText(result.file) in the useExportPage hook with the
app's shared clipboard utility (e.g., copyToClipboard or clipboard.writeText
helper); import the shared helper at the top of use-export-page.ts, call it with
result.file, and keep the same return true flow so feature detection/fallback
behavior is consistent with the rest of the app.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2787fc45-6745-4971-bad3-7616696af7b9
📒 Files selected for processing (1)
packages/frontend/core/src/components/hooks/affine/use-export-page.ts
packages/frontend/core/src/components/hooks/affine/use-export-page.ts
Outdated
Show resolved
Hide resolved
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## canary #14705 +/- ##
==========================================
- Coverage 57.82% 57.34% -0.49%
==========================================
Files 2960 2960
Lines 165735 165735
Branches 24428 24290 -138
==========================================
- Hits 95841 95044 -797
- Misses 66848 67560 +712
- Partials 3046 3131 +85
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@darkskygit is there any issue in the PR. Do I have follow some set of rules?? I will fix let me know. |
please be patient; I haven’t had time to review the pr yet |
|
need run |
I tried to run but getting some errors related to prisma while yarn install process. |
if you are run in mac or linux, you can run |
Fixes #12983
Summary by CodeRabbit
New Features
Behavior
Localization