From d8ec0472ef3e456f37803246c1b8d77a73c03abe Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:15:52 +0000 Subject: [PATCH] feat: PDF embed --- .../public/locales/en-US/translation.json | 5 + .../components/attachment/attachment-view.tsx | 53 +++++- .../editor/components/audio/audio-view.tsx | 16 +- .../common/editor-paste-handler.tsx | 5 + .../common/resizable-wrapper.module.css | 8 +- .../components/common/resizable-wrapper.tsx | 9 + .../editor/components/embed/embed-view.tsx | 5 +- .../editor/components/pdf/pdf-menu.tsx | 164 ++++++++++++++++++ .../editor/components/pdf/pdf-view.module.css | 78 +++++++++ .../editor/components/pdf/pdf-view.tsx | 107 ++++++++++++ .../components/pdf/upload-pdf-action.tsx | 36 ++++ .../components/slash-menu/menu-items.ts | 35 +++- .../features/editor/extensions/extensions.ts | 5 + .../src/features/editor/page-editor.tsx | 2 + .../src/features/editor/styles/media.css | 2 +- .../src/collaboration/collaboration.util.ts | 2 + .../src/common/helpers/prosemirror/utils.ts | 1 + .../core/attachment/attachment.controller.ts | 4 + .../services/import-attachment.service.ts | 22 ++- packages/editor-ext/src/index.ts | 1 + packages/editor-ext/src/lib/audio/audio.ts | 16 +- packages/editor-ext/src/lib/pdf/index.ts | 2 + packages/editor-ext/src/lib/pdf/pdf-upload.ts | 123 +++++++++++++ packages/editor-ext/src/lib/pdf/pdf.ts | 156 +++++++++++++++++ packages/editor-ext/src/lib/utils.ts | 6 + 25 files changed, 835 insertions(+), 28 deletions(-) create mode 100644 apps/client/src/features/editor/components/pdf/pdf-menu.tsx create mode 100644 apps/client/src/features/editor/components/pdf/pdf-view.module.css create mode 100644 apps/client/src/features/editor/components/pdf/pdf-view.tsx create mode 100644 apps/client/src/features/editor/components/pdf/upload-pdf-action.tsx create mode 100644 packages/editor-ext/src/lib/pdf/index.ts create mode 100644 packages/editor-ext/src/lib/pdf/pdf-upload.ts create mode 100644 packages/editor-ext/src/lib/pdf/pdf.ts diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index fc9e0717..68bef0f2 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -353,6 +353,11 @@ "Quote": "Quote.", "Image": "Image.", "Audio": "Audio.", + "Embed PDF": "Embed PDF", + "Upload and embed a PDF file.": "Upload and embed a PDF file.", + "Embed as PDF": "Embed as PDF", + "Failed to load PDF": "Failed to load PDF", + "Convert to attachment": "Convert to attachment", "File attachment": "File attachment.", "Toggle block": "Toggle block.", "Callout": "Callout.", diff --git a/apps/client/src/features/editor/components/attachment/attachment-view.tsx b/apps/client/src/features/editor/components/attachment/attachment-view.tsx index e3281e64..f6c13c80 100644 --- a/apps/client/src/features/editor/components/attachment/attachment-view.tsx +++ b/apps/client/src/features/editor/components/attachment/attachment-view.tsx @@ -1,17 +1,43 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; -import { Group, Text, Paper, ActionIcon, Loader } from "@mantine/core"; +import { Group, Text, Paper, ActionIcon, Loader, Tooltip } from "@mantine/core"; import { getFileUrl } from "@/lib/config.ts"; -import { IconDownload, IconPaperclip } from "@tabler/icons-react"; +import { IconDownload, IconFileTypePdf, IconPaperclip } from "@tabler/icons-react"; import { useHover } from "@mantine/hooks"; import { formatBytes } from "@/lib"; import { useTranslation } from "react-i18next"; +import { useCallback } from "react"; export default function AttachmentView(props: NodeViewProps) { const { t } = useTranslation(); - const { node, selected } = props; - const { url, name, size } = node.attrs; + const { editor, node, getPos, selected } = props; + const { url, name, size, mime, attachmentId } = node.attrs; const { hovered, ref } = useHover(); + const isPdf = mime === "application/pdf" || name?.toLowerCase().endsWith(".pdf"); + + const handleEmbedAsPdf = useCallback(() => { + const pos = getPos(); + if (pos === undefined || !url) return; + + const nodeSize = node.nodeSize; + + editor + .chain() + .insertContentAt( + { from: pos, to: pos + nodeSize }, + { + type: "pdf", + attrs: { + src: url, + name, + attachmentId, + size, + }, + }, + ) + .run(); + }, [editor, getPos, node, url, name, attachmentId]); + return ( @@ -39,11 +65,20 @@ export default function AttachmentView(props: NodeViewProps) { {url && (selected || hovered) && ( - - - - - + + {isPdf && editor.isEditable && ( + + + + + + )} + + + + + + )} diff --git a/apps/client/src/features/editor/components/audio/audio-view.tsx b/apps/client/src/features/editor/components/audio/audio-view.tsx index da08ccaa..a353ce45 100644 --- a/apps/client/src/features/editor/components/audio/audio-view.tsx +++ b/apps/client/src/features/editor/components/audio/audio-view.tsx @@ -2,6 +2,7 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { Group, Loader, Text } from "@mantine/core"; import { useMemo } from "react"; import { getFileUrl } from "@/lib/config.ts"; +import { isInternalFileUrl } from "@docmost/editor-ext"; import classes from "./audio-view.module.css"; import { useTranslation } from "react-i18next"; @@ -10,6 +11,11 @@ export default function AudioView(props: NodeViewProps) { const { editor, node } = props; const { src, placeholder } = node.attrs; + const safeSrc = useMemo(() => { + if (!src || !isInternalFileUrl(src)) return null; + return getFileUrl(src); + }, [src]); + const previewSrc = useMemo(() => { editor.storage.shared.audioPreviews = editor.storage.shared.audioPreviews || {}; @@ -23,16 +29,16 @@ export default function AudioView(props: NodeViewProps) { return ( -
- {src && ( +
+ {safeSrc && (