From 8f51f23d66d4eab997f622948f42612f49fada90 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 9 Jul 2026 10:37:53 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8(frontend)=20new=20custom=20block?= =?UTF-8?q?=20"embed"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We introduce a new custom block called "embed" that allows users to embed external content into their documents. This block can be used to include videos, interactive widgets, or other web content directly within the document editor. It is a powerfull custom block that can make your document much more interactive. --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-editor.spec.ts | 50 ++++- .../doc-editor/components/BlockNoteEditor.tsx | 12 +- .../components/BlockNoteSuggestionMenu.tsx | 2 + .../custom-blocks/CustomBlockStatus.tsx | 43 ++++ .../custom-blocks/DocsFilePanel.tsx | 81 ++++++++ .../components/custom-blocks/EmbedBlock.tsx | 190 ++++++++++++++++++ .../components/custom-blocks/PdfBlock.tsx | 15 +- .../custom-blocks/UploadLoaderBlock.tsx | 22 +- .../__tests__/EmbedBlock.test.ts | 31 +++ .../components/custom-blocks/index.ts | 2 + .../features/docs/doc-export/mappingDocx.tsx | 4 +- .../features/docs/doc-export/mappingODT.ts | 4 +- .../features/docs/doc-export/mappingPDF.tsx | 4 +- .../features/docs/doc-export/utils_print.ts | 14 +- 15 files changed, 439 insertions(+), 36 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CustomBlockStatus.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/DocsFilePanel.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/EmbedBlock.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/__tests__/EmbedBlock.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aafeb4ab4..d4516c8c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to ### Added +- ✨(frontend) new custom block "embed" #2513 - ♿️(frontend) restore skip to content link after header redesign #2510 ## [v5.4.1] - 2026-07-09 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index 0524167227..8b9afa11d1 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -544,7 +544,7 @@ test.describe('Doc Editor', () => { }); test('it embeds PDF', async ({ page, browserName }) => { - await createDoc(page, 'doc-toolbar', browserName, 1); + await createDoc(page, 'doc-embed-pdf', browserName, 1); await page.getByRole('button', { name: 'Share' }).click(); await updateShareLink(page, 'Public', 'Reading'); @@ -637,4 +637,52 @@ test.describe('Doc Editor', () => { await expect(editor.getByText('Mobile Text')).toBeVisible(); }); + + test('it embeds a web page', async ({ page, browserName }) => { + await createDoc(page, 'doc-embed-web', browserName, 1); + + await openSuggestionMenu({ page, suggestion: 'Embed a web page' }); + + const embedBlock = page.locator('div[data-content-type="embed"]').last(); + + await expect(embedBlock).toBeVisible(); + + // Try with same domain first + await page + .getByText(/Add embed/) + .first() + .click(); + + await page + .locator('[data-test="embed-input"]') + .fill('http://localhost:3000/'); + + await page.locator('[data-test="embed-input-button"]').click(); + + await expect(page.getByText('Invalid or unsafe URL.')).toBeVisible(); + + await openSuggestionMenu({ page, suggestion: 'Embed a web page' }); + + // Now with a valid URL + await page + .getByText(/Add embed/) + .first() + .click(); + + await page + .locator('[data-test="embed-input"]') + .fill('http://127.0.0.1:3000/'); + await page.locator('[data-test="embed-input-button"]').click(); + + const embedIframe = page + .locator('.--docs--editor-container iframe.bn-visual-media') + .first(); + + // Check src of embed iframe + expect(await embedIframe.getAttribute('src')).toMatch( + /http:\/\/127.0.0.1:3000\//, + ); + + await expect(embedIframe).toHaveAttribute('role', 'presentation'); + }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 5c9b2b1e29..7e4681ee89 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -12,6 +12,7 @@ import * as localesBN from '@blocknote/core/locales'; import { BlockNoteView } from '@blocknote/mantine'; import '@blocknote/mantine/style.css'; import { + FilePanelController, FloatingComposerController, FloatingThreadController, ThreadsSidebar, @@ -53,7 +54,13 @@ import { randomColor, sanitizeColor } from '../utils'; import BlockNoteAI from './AI'; import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu'; import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar'; -import { CalloutBlock, PdfBlock, UploadLoaderBlock } from './custom-blocks'; +import { + CalloutBlock, + DocsFilePanel, + EmbedBlock, + PdfBlock, + UploadLoaderBlock, +} from './custom-blocks'; const AIMenu = BlockNoteAI?.AIMenu; const AIMenuController = BlockNoteAI?.AIMenuController; const useAI = BlockNoteAI?.useAI; @@ -70,6 +77,7 @@ const baseBlockNoteSchema = withPageBreak( ...defaultBlockSpecs, callout: CalloutBlock(), codeBlock: createCodeBlockSpec(codeBlockOptions), + embed: EmbedBlock(), pdf: PdfBlock(), uploadLoader: UploadLoaderBlock(), }, @@ -292,6 +300,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { editor={editor} formattingToolbar={false} slashMenu={false} + filePanel={false} theme="light" comments={false} aria-label={t('Document editor')} @@ -303,6 +312,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { )} + {showComments && } {showComments && !isCommentSideBarOpen && } {threadsSidebarTarget && diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx index 805377f691..24127e00b5 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx @@ -20,6 +20,7 @@ import { import BlockNoteAI from './AI'; import { getCalloutReactSlashMenuItems, + getEmbedReactSlashMenuItems, getPdfReactSlashMenuItems, } from './custom-blocks'; import { useGetInterlinkingMenuItems } from './custom-inline-content'; @@ -56,6 +57,7 @@ export const BlockNoteSuggestionMenu = ({ getPageBreakReactSlashMenuItems(editor), getMultiColumnSlashMenuItems?.(editor) || [], getPdfReactSlashMenuItems(editor, t, fileBlocksName), + getEmbedReactSlashMenuItems(editor, t, fileBlocksName), getCalloutReactSlashMenuItems(editor, t, basicBlocksName), aiAllowed && getAISlashMenuItems ? getAISlashMenuItems(editor) : [], ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CustomBlockStatus.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CustomBlockStatus.tsx new file mode 100644 index 0000000000..fa8646874e --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CustomBlockStatus.tsx @@ -0,0 +1,43 @@ +import { Box, BoxType, Icon } from '@/components'; + +import Loader from '../../assets/loader.svg'; +import Warning from '../../assets/warning.svg'; + +interface CustomBlockStatusProps extends BoxType { + type?: 'loading' | 'warning'; +} + +export const CustomBlockStatus = ({ + type = 'warning', + children, + ...props +}: CustomBlockStatusProps) => { + return ( + + {type === 'warning' ? ( + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/DocsFilePanel.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/DocsFilePanel.tsx new file mode 100644 index 0000000000..6c694c9fb0 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/DocsFilePanel.tsx @@ -0,0 +1,81 @@ +/** + * Copy of Blocknote's default FilePanel, restricted to hide the "Upload" tab + * for blocks whose props explicitly set `uploadDisabled: true` (e.g. the + * "embed" block, which only supports embedding an existing URL). + * + * Original source: + * https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FilePanel/FilePanel.tsx + */ +import { + EmbedTab, + FilePanelProps, + UploadTab, + useBlockNoteEditor, + useComponentsContext, + useDictionary, +} from '@blocknote/react'; +import { useState } from 'react'; + +import { + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema, +} from '../../types'; + +export const DocsFilePanel = ( + props: FilePanelProps & Partial<{ defaultOpenTab: string }>, +) => { + const Components = useComponentsContext(); + const dict = useDictionary(); + + const editor = useBlockNoteEditor< + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema + >(); + + const [loading, setLoading] = useState(false); + + const block = editor.getBlock(props.blockId); + const uploadDisabled = + !!block && + 'uploadDisabled' in block.props && + block.props.uploadDisabled === true; + const allowUpload = editor.uploadFile !== undefined && !uploadDisabled; + + const tabs = [ + ...(allowUpload + ? [ + { + name: dict.file_panel.upload.title, + tabPanel: ( + + ), + }, + ] + : []), + { + name: dict.file_panel.embed.title, + tabPanel: , + }, + ]; + + const [openTab, setOpenTab] = useState( + props.defaultOpenTab || tabs[0].name, + ); + + if (!Components) { + return null; + } + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/EmbedBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/EmbedBlock.tsx new file mode 100644 index 0000000000..b63515bdb4 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/EmbedBlock.tsx @@ -0,0 +1,190 @@ +/** + * EmbedBlock — embeds an external web page in a document via a sandboxed + * `