✨(frontend) add export page break
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.
This commit is contained in:
@@ -14,6 +14,7 @@ and this project adheres to
|
|||||||
- 📝(doc) Add security.md and codeofconduct.md #604
|
- 📝(doc) Add security.md and codeofconduct.md #604
|
||||||
- ✨(frontend) add home page #553
|
- ✨(frontend) add home page #553
|
||||||
- ✨(frontend) cursor display on activity #609
|
- ✨(frontend) cursor display on activity #609
|
||||||
|
- ✨(frontend) Add export page break #623
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,16 @@ test.describe('Doc Export', () => {
|
|||||||
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it exports the doc to pdf', async ({ page, browserName }) => {
|
test('it exports the doc with pdf line break', async ({
|
||||||
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [randomDoc] = await createDoc(
|
||||||
|
page,
|
||||||
|
'doc-editor-line-break',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||||
@@ -50,8 +58,20 @@ test.describe('Doc Export', () => {
|
|||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
await verifyDocName(page, randomDoc);
|
||||||
|
|
||||||
await page.locator('.ProseMirror.bn-editor').click();
|
const editor = page.locator('.ProseMirror.bn-editor');
|
||||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
|
||||||
|
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
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
@@ -69,9 +89,10 @@ test.describe('Doc Export', () => {
|
|||||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||||
|
|
||||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
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 }) => {
|
test('it exports the doc to docx', async ({ page, browserName }) => {
|
||||||
|
|||||||
@@ -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 '@blocknote/core/fonts/inter.css';
|
||||||
import { BlockNoteView } from '@blocknote/mantine';
|
import { BlockNoteView } from '@blocknote/mantine';
|
||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
@@ -19,6 +24,7 @@ import useSaveDoc from '../hook/useSaveDoc';
|
|||||||
import { useEditorStore } from '../stores';
|
import { useEditorStore } from '../stores';
|
||||||
import { randomColor } from '../utils';
|
import { randomColor } from '../utils';
|
||||||
|
|
||||||
|
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||||
|
|
||||||
const cssEditor = (readonly: boolean) => css`
|
const cssEditor = (readonly: boolean) => css`
|
||||||
@@ -142,6 +148,8 @@ const cssEditor = (readonly: boolean) => css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const blockNoteSchema = withPageBreak(BlockNoteSchema.create());
|
||||||
|
|
||||||
interface BlockNoteEditorProps {
|
interface BlockNoteEditorProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
provider: HocuspocusProvider;
|
provider: HocuspocusProvider;
|
||||||
@@ -217,6 +225,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
},
|
},
|
||||||
dictionary: locales[lang as keyof typeof locales] as Dictionary,
|
dictionary: locales[lang as keyof typeof locales] as Dictionary,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
|
schema: blockNoteSchema,
|
||||||
},
|
},
|
||||||
[collabName, lang, provider, uploadFile],
|
[collabName, lang, provider, uploadFile],
|
||||||
);
|
);
|
||||||
@@ -249,10 +258,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
<BlockNoteView
|
<BlockNoteView
|
||||||
editor={editor}
|
editor={editor}
|
||||||
formattingToolbar={false}
|
formattingToolbar={false}
|
||||||
|
slashMenu={false}
|
||||||
editable={!readOnly}
|
editable={!readOnly}
|
||||||
theme="light"
|
theme="light"
|
||||||
>
|
>
|
||||||
<BlockNoteToolbar />
|
<BlockNoteToolbar />
|
||||||
|
<BlockNoteSuggestionMenu />
|
||||||
</BlockNoteView>
|
</BlockNoteView>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -277,6 +288,7 @@ export const BlockNoteEditorVersion = ({
|
|||||||
},
|
},
|
||||||
provider: undefined,
|
provider: undefined,
|
||||||
},
|
},
|
||||||
|
schema: blockNoteSchema,
|
||||||
},
|
},
|
||||||
[initialContent],
|
[initialContent],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<SuggestionMenuController
|
||||||
|
triggerCharacter="/"
|
||||||
|
getItems={getSlashMenuItems}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { BlockNoteEditor } from '@blocknote/core';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { useHeadingStore } from '../stores';
|
import { useHeadingStore } from '../stores';
|
||||||
|
import { DocsBlockNoteEditor } from '../types';
|
||||||
|
|
||||||
export const useHeadings = (editor: BlockNoteEditor) => {
|
export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
||||||
const { setHeadings, resetHeadings } = useHeadingStore();
|
const { setHeadings, resetHeadings } = useHeadingStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { BlockNoteEditor } from '@blocknote/core';
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { DocsBlockNoteEditor } from '../types';
|
||||||
|
|
||||||
export interface UseEditorstore {
|
export interface UseEditorstore {
|
||||||
editor?: BlockNoteEditor;
|
editor?: DocsBlockNoteEditor;
|
||||||
setEditor: (editor: BlockNoteEditor | undefined) => void;
|
setEditor: (editor: DocsBlockNoteEditor | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEditorStore = create<UseEditorstore>((set) => ({
|
export const useEditorStore = create<UseEditorstore>((set) => ({
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { BlockNoteEditor } from '@blocknote/core';
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { HeadingBlock } from '../types';
|
import { DocsBlockNoteEditor, HeadingBlock } from '../types';
|
||||||
|
|
||||||
const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
@@ -21,7 +20,7 @@ const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
|||||||
|
|
||||||
export interface UseHeadingStore {
|
export interface UseHeadingStore {
|
||||||
headings: HeadingBlock[];
|
headings: HeadingBlock[];
|
||||||
setHeadings: (editor: BlockNoteEditor) => void;
|
setHeadings: (editor: DocsBlockNoteEditor) => void;
|
||||||
resetHeadings: () => void;
|
resetHeadings: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { BlockNoteEditor } from '@blocknote/core';
|
||||||
|
|
||||||
|
import { blockNoteSchema } from './components/BlockNoteEditor';
|
||||||
|
|
||||||
export interface DocAttachment {
|
export interface DocAttachment {
|
||||||
file: string;
|
file: string;
|
||||||
}
|
}
|
||||||
@@ -12,3 +16,9 @@ export type HeadingBlock = {
|
|||||||
level: number;
|
level: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DocsBlockNoteEditor = BlockNoteEditor<
|
||||||
|
typeof blockNoteSchema.blockSchema,
|
||||||
|
typeof blockNoteSchema.inlineContentSchema,
|
||||||
|
typeof blockNoteSchema.styleSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { BlockNoteEditor } from '@blocknote/core';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { BoxButton, Text } from '@/components';
|
import { BoxButton, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { DocsBlockNoteEditor } from '@/features/docs/doc-editor';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
const leftPaddingMap: { [key: number]: string } = {
|
const leftPaddingMap: { [key: number]: string } = {
|
||||||
@@ -17,7 +17,7 @@ export type HeadingsHighlight = {
|
|||||||
}[];
|
}[];
|
||||||
|
|
||||||
interface HeadingProps {
|
interface HeadingProps {
|
||||||
editor: BlockNoteEditor;
|
editor: DocsBlockNoteEditor;
|
||||||
level: number;
|
level: number;
|
||||||
text: string;
|
text: string;
|
||||||
headingId: string;
|
headingId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user