From f5f9d8a8773a7e560cbee89923c459ca2c093cfc Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 18 Jul 2025 17:35:12 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20interlinking=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create interlinking link mapping for docx and pdf export. --- .../__tests__/app-impress/doc-export.spec.ts | 69 ++++++++++++++++++ .../docs/doc-export/assets/doc-selected.png | Bin 0 -> 338 bytes .../blocks-mapping/paragraphPDF.tsx | 2 +- .../doc-export/blocks-mapping/quoteDocx.tsx | 16 ++-- .../inline-content-mapping/index.ts | 2 + .../interlinkingLinkDocx.tsx | 16 ++++ .../interlinkingLinkPDF.tsx | 22 ++++++ .../features/docs/doc-export/mappingDocx.tsx | 7 ++ .../features/docs/doc-export/mappingPDF.tsx | 6 ++ 9 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/assets/doc-selected.png create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/index.ts create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkDocx.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkPDF.tsx 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 b70ebd01..f66aecf8 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 @@ -5,6 +5,7 @@ import cs from 'convert-stream'; import pdf from 'pdf-parse'; import { createDoc, verifyDocName } from './utils-common'; +import { createRootSubPage } from './utils-sub-pages'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -411,4 +412,72 @@ test.describe('Doc Export', () => { expect(pdfData.text).toContain('Column 2'); expect(pdfData.text).toContain('Column 3'); }); + + test('it exports the doc with interlinking', async ({ + page, + browserName, + }) => { + const [randomDoc] = await createDoc( + page, + 'export-interlinking', + browserName, + 1, + ); + + await verifyDocName(page, randomDoc); + + const { name: docChild } = await createRootSubPage( + page, + browserName, + 'export-interlink-child', + ); + + await verifyDocName(page, docChild); + + await page.locator('.bn-block-outer').last().fill('/'); + await page.getByText('Link a doc').first().click(); + + await page + .locator( + "span[data-inline-content-type='interlinkingSearchInline'] input", + ) + .fill('interlink-child'); + + await page + .locator('.quick-search-container') + .getByText('interlink-child') + .click(); + + const interlink = page.getByRole('link', { + name: 'interlink-child', + }); + + await expect(interlink).toBeVisible(); + + const downloadPromise = page.waitForEvent('download', (download) => { + return download.suggestedFilename().includes(`${docChild}.pdf`); + }); + + await page + .getByRole('button', { + name: 'download', + exact: true, + }) + .click(); + + void page + .getByRole('button', { + name: 'Download', + exact: true, + }) + .click(); + + const download = await downloadPromise; + expect(download.suggestedFilename()).toBe(`${docChild}.pdf`); + + const pdfBuffer = await cs.toBuffer(await download.createReadStream()); + const pdfData = await pdf(pdfBuffer); + + expect(pdfData.text).toContain('interlink-child'); // This is the pdf text + }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/assets/doc-selected.png b/src/frontend/apps/impress/src/features/docs/doc-export/assets/doc-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..d4375fb49e3b95f524e65ae2b82cde3e984b14f3 GIT binary patch literal 338 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eK!3HFi66di4Db50q$YKTtZeb8+WSBKa0w~B> z9OUlAup(vvTn;DLzZGSyCB=0t#kFT)tG(DA { - if (content.type === 'text' && !content.text) { + if (content.type === 'text' && 'text' in content && !content.text) { content.text = ' '; } }); 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 index bcdaa68c..1691386f 100644 --- 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 @@ -8,11 +8,17 @@ export const blockMappingQuoteDocx: DocsExporterDocx['mappings']['blockMapping'] if (Array.isArray(block.content)) { block.content.forEach((content) => { if (content.type === 'text') { - content.styles = { - ...content.styles, - italic: true, - textColor: 'gray', - }; + if ( + 'styles' in content && + typeof content.styles === 'object' && + content.styles !== null + ) { + content.styles = { + ...content.styles, + italic: true, + textColor: 'gray', + }; + } } }); } diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/index.ts b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/index.ts new file mode 100644 index 00000000..0b037c17 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/index.ts @@ -0,0 +1,2 @@ +export * from './interlinkingLinkPDF'; +export * from './interlinkingLinkDocx'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkDocx.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkDocx.tsx new file mode 100644 index 00000000..afb8c0e7 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkDocx.tsx @@ -0,0 +1,16 @@ +import { ExternalHyperlink, TextRun } from 'docx'; + +import { DocsExporterDocx } from '../types'; + +export const inlineContentMappingInterlinkingLinkDocx: DocsExporterDocx['mappings']['inlineContentMapping']['interlinkingLinkInline'] = + (inline) => { + return new ExternalHyperlink({ + children: [ + new TextRun({ + text: `📄${inline.props.title}`, + bold: true, + }), + ], + link: window.location.origin + inline.props.url, + }); + }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkPDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkPDF.tsx new file mode 100644 index 00000000..15732722 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkPDF.tsx @@ -0,0 +1,22 @@ +/* eslint-disable jsx-a11y/alt-text */ +import { Image, Link, Text } from '@react-pdf/renderer'; + +import DocSelectedIcon from '../assets/doc-selected.png'; +import { DocsExporterPDF } from '../types'; + +export const inlineContentMappingInterlinkingLinkPDF: DocsExporterPDF['mappings']['inlineContentMapping']['interlinkingLinkInline'] = + (inline) => { + return ( + + {' '} + {' '} + {inline.props.title}{' '} + + ); + }; 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 434daa99..46263b92 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,4 +1,5 @@ import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter'; +import { Paragraph } from 'docx'; import { blockMappingCalloutDocx, @@ -6,6 +7,7 @@ import { blockMappingImageDocx, blockMappingQuoteDocx, } from './blocks-mapping'; +import { inlineContentMappingInterlinkingLinkDocx } from './inline-content-mapping'; import { DocsExporterDocx } from './types'; export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = { @@ -17,4 +19,9 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = { quote: blockMappingQuoteDocx, image: blockMappingImageDocx, }, + inlineContentMapping: { + ...docxDefaultSchemaMappings.inlineContentMapping, + interlinkingSearchInline: () => new Paragraph(''), + interlinkingLinkInline: inlineContentMappingInterlinkingLinkDocx, + }, }; 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 4224045d..53cc9061 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 @@ -9,6 +9,7 @@ import { blockMappingQuotePDF, blockMappingTablePDF, } from './blocks-mapping'; +import { inlineContentMappingInterlinkingLinkPDF } from './inline-content-mapping'; import { DocsExporterPDF } from './types'; export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = { @@ -23,4 +24,9 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = { quote: blockMappingQuotePDF, table: blockMappingTablePDF, }, + inlineContentMapping: { + ...pdfDefaultSchemaMappings.inlineContentMapping, + interlinkingSearchInline: () => <>, + interlinkingLinkInline: inlineContentMappingInterlinkingLinkPDF, + }, };