From a6b3cfdb0c344cba52307996493b186e36ec8f19 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 10 Feb 2025 12:05:21 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20export=20page=20bre?= =?UTF-8?q?ak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blocknotejs introduced the ability to export a document with page breaks. This commit adds the page break feature to the editor and so to our export feature. --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-export.spec.ts | 33 +++++++++++++---- .../doc-editor/components/BlockNoteEditor.tsx | 14 +++++++- .../components/BlockNoteSuggestionMenu.tsx | 35 +++++++++++++++++++ .../docs/doc-editor/hook/useHeadings.tsx | 4 +-- .../docs/doc-editor/stores/useEditorStore.tsx | 7 ++-- .../doc-editor/stores/useHeadingStore.tsx | 5 ++- .../src/features/docs/doc-editor/types.tsx | 10 ++++++ .../doc-table-content/components/Heading.tsx | 4 +-- 9 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index ceab904f..79c5a72b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to - 📝(doc) Add security.md and codeofconduct.md #604 - ✨(frontend) add home page #553 - ✨(frontend) cursor display on activity #609 +- ✨(frontend) Add export page break #623 ## Fixed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts index db9cb820..2bce4061 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts @@ -41,8 +41,16 @@ test.describe('Doc Export', () => { await expect(page.getByRole('button', { name: 'Download' })).toBeVisible(); }); - test('it exports the doc to pdf', async ({ page, browserName }) => { - const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1); + test('it exports the doc with pdf line break', async ({ + page, + browserName, + }) => { + const [randomDoc] = await createDoc( + page, + 'doc-editor-line-break', + browserName, + 1, + ); const downloadPromise = page.waitForEvent('download', (download) => { return download.suggestedFilename().includes(`${randomDoc}.pdf`); @@ -50,8 +58,20 @@ test.describe('Doc Export', () => { await verifyDocName(page, randomDoc); - await page.locator('.ProseMirror.bn-editor').click(); - await page.locator('.ProseMirror.bn-editor').fill('Hello World'); + const editor = page.locator('.ProseMirror.bn-editor'); + + await editor.click(); + await editor.locator('.bn-block-outer').last().fill('Hello'); + + await page.keyboard.press('Enter'); + await editor.locator('.bn-block-outer').last().fill('/'); + await page.getByText('Page Break').click(); + + await expect(editor.locator('.bn-page-break')).toBeVisible(); + + await page.keyboard.press('Enter'); + + await editor.locator('.bn-block-outer').last().fill('World'); await page .getByRole('button', { @@ -69,9 +89,10 @@ test.describe('Doc Export', () => { expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`); const pdfBuffer = await cs.toBuffer(await download.createReadStream()); - const pdfText = (await pdf(pdfBuffer)).text; + const pdfData = await pdf(pdfBuffer); - expect(pdfText).toContain('Hello World'); // This is the doc text + expect(pdfData.numpages).toBe(2); + expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text }); test('it exports the doc to docx', async ({ page, browserName }) => { 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 af4694c7..e0d5daf0 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 @@ -1,4 +1,9 @@ -import { Dictionary, locales } from '@blocknote/core'; +import { + BlockNoteSchema, + Dictionary, + locales, + withPageBreak, +} from '@blocknote/core'; import '@blocknote/core/fonts/inter.css'; import { BlockNoteView } from '@blocknote/mantine'; import '@blocknote/mantine/style.css'; @@ -19,6 +24,7 @@ import useSaveDoc from '../hook/useSaveDoc'; import { useEditorStore } from '../stores'; import { randomColor } from '../utils'; +import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu'; import { BlockNoteToolbar } from './BlockNoteToolbar'; const cssEditor = (readonly: boolean) => css` @@ -142,6 +148,8 @@ const cssEditor = (readonly: boolean) => css` } `; +export const blockNoteSchema = withPageBreak(BlockNoteSchema.create()); + interface BlockNoteEditorProps { doc: Doc; provider: HocuspocusProvider; @@ -217,6 +225,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { }, dictionary: locales[lang as keyof typeof locales] as Dictionary, uploadFile, + schema: blockNoteSchema, }, [collabName, lang, provider, uploadFile], ); @@ -249,10 +258,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { + ); @@ -277,6 +288,7 @@ export const BlockNoteEditorVersion = ({ }, provider: undefined, }, + schema: blockNoteSchema, }, [initialContent], ); 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 new file mode 100644 index 00000000..22b11365 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx @@ -0,0 +1,35 @@ +import { combineByGroup, filterSuggestionItems } from '@blocknote/core'; +import '@blocknote/mantine/style.css'; +import { + SuggestionMenuController, + getDefaultReactSlashMenuItems, + getPageBreakReactSlashMenuItems, + useBlockNoteEditor, +} from '@blocknote/react'; +import React, { useMemo } from 'react'; + +import { DocsBlockNoteEditor } from '../types'; + +export const BlockNoteSuggestionMenu = () => { + const editor = useBlockNoteEditor() as DocsBlockNoteEditor; + + const getSlashMenuItems = useMemo(() => { + return async (query: string) => + Promise.resolve( + filterSuggestionItems( + combineByGroup( + getDefaultReactSlashMenuItems(editor), + getPageBreakReactSlashMenuItems(editor), + ), + query, + ), + ); + }, [editor]); + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx index 9468a796..8b88eb3d 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx @@ -1,9 +1,9 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { useEffect } from 'react'; import { useHeadingStore } from '../stores'; +import { DocsBlockNoteEditor } from '../types'; -export const useHeadings = (editor: BlockNoteEditor) => { +export const useHeadings = (editor: DocsBlockNoteEditor) => { const { setHeadings, resetHeadings } = useHeadingStore(); useEffect(() => { diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useEditorStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useEditorStore.tsx index 025f2ad8..9a846b37 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useEditorStore.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useEditorStore.tsx @@ -1,9 +1,10 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { create } from 'zustand'; +import { DocsBlockNoteEditor } from '../types'; + export interface UseEditorstore { - editor?: BlockNoteEditor; - setEditor: (editor: BlockNoteEditor | undefined) => void; + editor?: DocsBlockNoteEditor; + setEditor: (editor: DocsBlockNoteEditor | undefined) => void; } export const useEditorStore = create((set) => ({ diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx index ac9b8a4b..e67800c6 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx @@ -1,7 +1,6 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { create } from 'zustand'; -import { HeadingBlock } from '../types'; +import { DocsBlockNoteEditor, HeadingBlock } from '../types'; const recursiveTextContent = (content: HeadingBlock['content']): string => { if (!content) { @@ -21,7 +20,7 @@ const recursiveTextContent = (content: HeadingBlock['content']): string => { export interface UseHeadingStore { headings: HeadingBlock[]; - setHeadings: (editor: BlockNoteEditor) => void; + setHeadings: (editor: DocsBlockNoteEditor) => void; resetHeadings: () => void; } diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx index 19094b6c..2bb97a4b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx @@ -1,3 +1,7 @@ +import { BlockNoteEditor } from '@blocknote/core'; + +import { blockNoteSchema } from './components/BlockNoteEditor'; + export interface DocAttachment { file: string; } @@ -12,3 +16,9 @@ export type HeadingBlock = { level: number; }; }; + +export type DocsBlockNoteEditor = BlockNoteEditor< + typeof blockNoteSchema.blockSchema, + typeof blockNoteSchema.inlineContentSchema, + typeof blockNoteSchema.styleSchema +>; diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx index 032a44d5..6445fc67 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx @@ -1,8 +1,8 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { useState } from 'react'; import { BoxButton, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; +import { DocsBlockNoteEditor } from '@/features/docs/doc-editor'; import { useResponsiveStore } from '@/stores'; const leftPaddingMap: { [key: number]: string } = { @@ -17,7 +17,7 @@ export type HeadingsHighlight = { }[]; interface HeadingProps { - editor: BlockNoteEditor; + editor: DocsBlockNoteEditor; level: number; text: string; headingId: string;