From cd5ee3fb7ca16766ad5213bfafa254cc0719a199 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 20 Feb 2025 10:03:11 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20adapt=20export=20to=20quo?= =?UTF-8?q?te=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have a new block type, the quote block. We have to adapt the export to handle this new block type. --- CHANGELOG.md | 2 + .../__tests__/app-impress/doc-export.spec.ts | 45 +++++++++++++++++++ .../docs/doc-editor/components/index.ts | 1 + .../docs/doc-export/blocks-mapping/index.ts | 2 + .../doc-export/blocks-mapping/quoteDocx.tsx | 33 ++++++++++++++ .../doc-export/blocks-mapping/quotePDF.tsx | 21 +++++++++ .../features/docs/doc-export/mappingDocx.tsx | 2 + .../features/docs/doc-export/mappingPDF.tsx | 2 + .../src/features/docs/doc-export/utils.ts | 43 ++++++++++++++++++ 9 files changed, 151 insertions(+) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/quoteDocx.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/quotePDF.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f81c5a00..81286ad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to ## Added - 💄(frontend) add error pages #643 +- ✨(frontend) Custom block quote with export #646 ## Changed @@ -24,6 +25,7 @@ and this project adheres to - 🐛(backend) allow any type of extensions for media download #671 - ♻️(frontend) improve table pdf rendering + ## [2.2.0] - 2025-02-10 ## Added 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 2bce4061..42f3ed90 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 @@ -197,4 +197,49 @@ test.describe('Doc Export', () => { expect(pdfText).toContain('Hello World'); }); + + test('it exports the doc with quotes', async ({ page, browserName }) => { + const [randomDoc] = await createDoc(page, 'export-quotes', browserName, 1); + + const downloadPromise = page.waitForEvent('download', (download) => { + return download.suggestedFilename().includes(`${randomDoc}.pdf`); + }); + + const editor = page.locator('.ProseMirror'); + // Trigger slash menu to show menu + await editor.click(); + await editor.fill('/'); + await page.getByText('Add a quote block').click(); + + await expect( + editor.locator('.bn-block-content[data-content-type="quote"]'), + ).toBeVisible(); + + await editor.fill('Hello World'); + + await expect(editor.getByText('Hello World')).toHaveCSS( + 'font-style', + 'italic', + ); + + await page + .getByRole('button', { + name: 'download', + }) + .click(); + + await page + .getByRole('button', { + name: 'Download', + }) + .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('Hello World'); // This is the pdf text + }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts index 6d59303e..643b57fa 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts @@ -1 +1,2 @@ export * from './DocEditor'; +export * from './custom-blocks/'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/index.ts b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/index.ts index 36f9c79f..a4fd91a2 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/index.ts @@ -1,3 +1,5 @@ export * from './headingPDF'; export * from './paragraphPDF'; +export * from './quoteDocx'; +export * from './quotePDF'; export * from './tablePDF'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/quoteDocx.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/quoteDocx.tsx new file mode 100644 index 00000000..bcdaa68c --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/quoteDocx.tsx @@ -0,0 +1,33 @@ +import { Paragraph } from 'docx'; + +import { DocsExporterDocx } from '../types'; +import { docxBlockPropsToStyles } from '../utils'; + +export const blockMappingQuoteDocx: DocsExporterDocx['mappings']['blockMapping']['quote'] = + (block, exporter) => { + if (Array.isArray(block.content)) { + block.content.forEach((content) => { + if (content.type === 'text') { + content.styles = { + ...content.styles, + italic: true, + textColor: 'gray', + }; + } + }); + } + + return new Paragraph({ + ...docxBlockPropsToStyles(block.props, exporter.options.colors), + spacing: { before: 10, after: 10 }, + border: { + left: { + color: '#cecece', + space: 4, + style: 'thick', + }, + }, + style: 'Normal', + children: exporter.transformInlineContent(block.content), + }); + }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/quotePDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/quotePDF.tsx new file mode 100644 index 00000000..0fd88d75 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/quotePDF.tsx @@ -0,0 +1,21 @@ +import { Text } from '@react-pdf/renderer'; + +import { DocsExporterPDF } from '../types'; + +export const blockMappingQuotePDF: DocsExporterPDF['mappings']['blockMapping']['quote'] = + (block, exporter) => { + return ( + + {exporter.transformInlineContent(block.content)} + + ); + }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx index 68b90803..c34f28fb 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx @@ -1,10 +1,12 @@ import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter'; +import { blockMappingQuoteDocx } from './blocks-mapping/'; import { DocsExporterDocx } from './types'; export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = { ...docxDefaultSchemaMappings, blockMapping: { ...docxDefaultSchemaMappings.blockMapping, + quote: blockMappingQuoteDocx, }, }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx index 19c9f204..d4ca9364 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx @@ -3,6 +3,7 @@ import { pdfDefaultSchemaMappings } from '@blocknote/xl-pdf-exporter'; import { blockMappingHeadingPDF, blockMappingParagraphPDF, + blockMappingQuotePDF, blockMappingTablePDF, } from './blocks-mapping'; import { DocsExporterPDF } from './types'; @@ -13,6 +14,7 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = { ...pdfDefaultSchemaMappings.blockMapping, heading: blockMappingHeadingPDF, paragraph: blockMappingParagraphPDF, + quote: blockMappingQuotePDF, table: blockMappingTablePDF, }, }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-export/utils.ts index d4a6165d..54a15a25 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/utils.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-export/utils.ts @@ -1,3 +1,10 @@ +import { + COLORS_DEFAULT, + DefaultProps, + UnreachableCaseError, +} from '@blocknote/core'; +import { IParagraphOptions, ShadingType } from 'docx'; + export function downloadFile(blob: Blob, filename: string) { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -30,3 +37,39 @@ export const exportResolveFileUrl = async ( return url; }; + +export function docxBlockPropsToStyles( + props: Partial, + colors: typeof COLORS_DEFAULT, +): IParagraphOptions { + return { + shading: + props.backgroundColor === 'default' || !props.backgroundColor + ? undefined + : { + type: ShadingType.SOLID, + color: + colors[ + props.backgroundColor as keyof typeof colors + ].background.slice(1), + }, + run: + props.textColor === 'default' || !props.textColor + ? undefined + : { + color: colors[props.textColor as keyof typeof colors].text.slice(1), + }, + alignment: + !props.textAlignment || props.textAlignment === 'left' + ? undefined + : props.textAlignment === 'center' + ? 'center' + : props.textAlignment === 'right' + ? 'right' + : props.textAlignment === 'justify' + ? 'distribute' + : (() => { + throw new UnreachableCaseError(props.textAlignment); + })(), + }; +}