diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aafeb4ab..d4516c8c2 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 052416722..8b9afa11d 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 5c9b2b1e2..7e4681ee8 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 805377f69..24127e00b 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 000000000..fa8646874 --- /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 000000000..6c694c9fb --- /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 000000000..b63515bdb --- /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 + * `