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 00000000..d4375fb4 Binary files /dev/null and b/src/frontend/apps/impress/src/features/docs/doc-export/assets/doc-selected.png differ diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/paragraphPDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/paragraphPDF.tsx index 19c253b7..89ebb5ea 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/paragraphPDF.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/paragraphPDF.tsx @@ -10,7 +10,7 @@ export const blockMappingParagraphPDF: DocsExporterPDF['mappings']['blockMapping */ if (Array.isArray(block.content)) { block.content.forEach((content) => { - 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, + }, };