diff --git a/CHANGELOG.md b/CHANGELOG.md index a2dc252402..89a4e8fe26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to - ✨(backend) allow masking documents from the list view #1171 - ✨(frontend) subdocs can manage link reach #1190 - ✨(frontend) add duplicate action to doc tree #1175 +- ✨(frontend) add multi columns support for editor #1219 ### Changed diff --git a/docs/env.md b/docs/env.md index 5f239ff2c3..ebb48215ef 100644 --- a/docs/env.md +++ b/docs/env.md @@ -136,9 +136,10 @@ NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build Packages with licences incompatible with the MIT licence: * `xl-docx-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE), -* `xl-pdf-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE) +* `xl-pdf-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE), +* `xl-multi-column`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/LICENSE). In `.env.development`, `PUBLISH_AS_MIT` is set to `false`, allowing developers to test Docs with all its features. -⚠️ If you run Docs in production with `PUBLISH_AS_MIT` set to `false` make sure you fulfill your [BlockNote licensing](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE) or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations. +⚠️ If you run Docs in production with `PUBLISH_AS_MIT` set to `false` make sure you fulfill your BlockNote licensing or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations. 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 ed4f60ea26..b70ebd0111 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 @@ -346,4 +346,69 @@ test.describe('Doc Export', () => { const pdfData = await pdf(pdfBuffer); expect(pdfData.text).toContain('Hello World'); }); + + test('it exports the doc with multi columns', async ({ + page, + browserName, + }) => { + const [randomDoc] = await createDoc( + page, + 'doc-multi-columns', + browserName, + 1, + ); + + await page.locator('.bn-block-outer').last().fill('/'); + + await page.getByText('Three Columns', { exact: true }).click(); + + await page.locator('.bn-block-column').first().fill('Column 1'); + await page.locator('.bn-block-column').nth(1).fill('Column 2'); + await page.locator('.bn-block-column').last().fill('Column 3'); + + expect(await page.locator('.bn-block-column').count()).toBe(3); + await expect( + page.locator('.bn-block-column[data-node-type="column"]').first(), + ).toHaveText('Column 1'); + await expect( + page.locator('.bn-block-column[data-node-type="column"]').nth(1), + ).toHaveText('Column 2'); + await expect( + page.locator('.bn-block-column[data-node-type="column"]').last(), + ).toHaveText('Column 3'); + + await page + .getByRole('button', { + name: 'download', + exact: true, + }) + .click(); + + await expect( + page.getByRole('button', { + name: 'Download', + exact: true, + }), + ).toBeVisible(); + + const downloadPromise = page.waitForEvent('download', (download) => { + return download.suggestedFilename().includes(`${randomDoc}.pdf`); + }); + + void page + .getByRole('button', { + name: 'Download', + exact: true, + }) + .click(); + + const download = await downloadPromise; + expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`); + + const pdfBuffer = await cs.toBuffer(await download.createReadStream()); + const pdfData = await pdf(pdfBuffer); + expect(pdfData.text).toContain('Column 1'); + expect(pdfData.text).toContain('Column 2'); + expect(pdfData.text).toContain('Column 3'); + }); }); diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index ec23c2ec48..4ad3ed5c73 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -21,6 +21,7 @@ "@blocknote/mantine": "0.34.0", "@blocknote/react": "0.34.0", "@blocknote/xl-docx-exporter": "0.34.0", + "@blocknote/xl-multi-column": "0.34.0", "@blocknote/xl-pdf-exporter": "0.34.0", "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", 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 fe419b809d..6709e97cef 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 @@ -28,8 +28,13 @@ import { randomColor } from '../utils'; import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu'; import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar'; import { CalloutBlock, DividerBlock } from './custom-blocks'; +import XLMultiColumn from './xl-multi-column'; -export const blockNoteSchema = withPageBreak( +const multiColumnDropCursor = XLMultiColumn?.multiColumnDropCursor; +const multiColumnLocales = XLMultiColumn?.locales; +const withMultiColumn = XLMultiColumn?.withMultiColumn; + +const baseBlockNoteSchema = withPageBreak( BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, @@ -39,6 +44,9 @@ export const blockNoteSchema = withPageBreak( }), ); +export const blockNoteSchema = (withMultiColumn?.(baseBlockNoteSchema) || + baseBlockNoteSchema) as typeof baseBlockNoteSchema; + interface BlockNoteEditorProps { doc: Doc; provider: HocuspocusProvider; @@ -116,7 +124,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { }, showCursorLabels: showCursorLabels as 'always' | 'activity', }, - dictionary: locales[lang as keyof typeof locales], + dictionary: { + ...locales[lang as keyof typeof locales], + multi_column: + multiColumnLocales?.[lang as keyof typeof multiColumnLocales], + }, tables: { splitCells: true, cellBackgroundColor: true, @@ -125,6 +137,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { }, uploadFile, schema: blockNoteSchema, + dropCursor: multiColumnDropCursor, }, [collabName, lang, provider, uploadFile], ); 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 3122b1c175..9284e81f23 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 @@ -15,6 +15,10 @@ import { getCalloutReactSlashMenuItems, getDividerReactSlashMenuItems, } from './custom-blocks'; +import XLMultiColumn from './xl-multi-column'; + +const getMultiColumnSlashMenuItems = + XLMultiColumn?.getMultiColumnSlashMenuItems; export const BlockNoteSuggestionMenu = () => { const editor = useBlockNoteEditor(); @@ -27,8 +31,9 @@ export const BlockNoteSuggestionMenu = () => { filterSuggestionItems( combineByGroup( getDefaultReactSlashMenuItems(editor), - getPageBreakReactSlashMenuItems(editor), getCalloutReactSlashMenuItems(editor, t, basicBlocksName), + getMultiColumnSlashMenuItems?.(editor) || [], + getPageBreakReactSlashMenuItems(editor), getDividerReactSlashMenuItems(editor, t, basicBlocksName), ), query, diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/xl-multi-column/index.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/xl-multi-column/index.ts new file mode 100644 index 0000000000..301c34dd64 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/xl-multi-column/index.ts @@ -0,0 +1,15 @@ +/** + * To import XL modules you must import from the index file. + * This is to ensure that the XL modules are only loaded when + * the application is not published as MIT. + */ +import * as XLMultiColumn from '@blocknote/xl-multi-column'; + +let modulesXL = undefined; +if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') { + modulesXL = XLMultiColumn; +} + +type ModulesXL = typeof XLMultiColumn | undefined; + +export default modulesXL as ModulesXL;