Skip to content

Commit 87cf9dc

Browse files
committed
✨(frontend) interlinking export
Create interlinking link mapping for docx and pdf export.
1 parent e6ad7d3 commit 87cf9dc

File tree

9 files changed

+166
-6
lines changed

9 files changed

+166
-6
lines changed

src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts

+68
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,72 @@ test.describe('Doc Export', () => {
311311
const pdfData = await pdf(pdfBuffer);
312312
expect(pdfData.text).toContain('Hello World');
313313
});
314+
315+
test('it exports the doc with interlinking', async ({
316+
page,
317+
browserName,
318+
}) => {
319+
const [randomDoc] = await createDoc(
320+
page,
321+
'export-interlinking',
322+
browserName,
323+
1,
324+
);
325+
326+
await verifyDocName(page, randomDoc);
327+
328+
const [docChild] = await createDoc(
329+
page,
330+
'export-interlink-child',
331+
browserName,
332+
1,
333+
true,
334+
);
335+
336+
await verifyDocName(page, docChild);
337+
338+
await page.locator('.bn-block-outer').last().fill('/');
339+
await page.getByText('Link to a page').first().click();
340+
341+
await page
342+
.locator(
343+
"span[data-inline-content-type='interlinkingSearchInline'] input",
344+
)
345+
.fill('interlink-child');
346+
347+
await page
348+
.locator('.quick-search-container')
349+
.getByText('interlink-child')
350+
.click();
351+
352+
const interlink = page.getByRole('link', {
353+
name: 'interlink-child',
354+
});
355+
356+
await expect(interlink).toBeVisible();
357+
358+
const downloadPromise = page.waitForEvent('download', (download) => {
359+
return download.suggestedFilename().includes(`${docChild}.pdf`);
360+
});
361+
362+
await page
363+
.getByRole('button', {
364+
name: 'download',
365+
})
366+
.click();
367+
368+
void page
369+
.getByRole('button', {
370+
name: 'Download',
371+
})
372+
.click();
373+
374+
const download = await downloadPromise;
375+
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
376+
377+
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
378+
const pdfData = await pdf(pdfBuffer);
379+
380+
expect(pdfData.text).toContain('interlink-child'); // This is the pdf text
381+
});
314382
});
Loading

src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/paragraphPDF.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const blockMappingParagraphPDF: DocsExporterPDF['mappings']['blockMapping
1010
*/
1111
if (Array.isArray(block.content)) {
1212
block.content.forEach((content) => {
13-
if (content.type === 'text' && !content.text) {
13+
if (content.type === 'text' && 'text' in content && !content.text) {
1414
content.text = ' ';
1515
}
1616
});

src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/quoteDocx.tsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ export const blockMappingQuoteDocx: DocsExporterDocx['mappings']['blockMapping']
88
if (Array.isArray(block.content)) {
99
block.content.forEach((content) => {
1010
if (content.type === 'text') {
11-
content.styles = {
12-
...content.styles,
13-
italic: true,
14-
textColor: 'gray',
15-
};
11+
if (
12+
'styles' in content &&
13+
typeof content.styles === 'object' &&
14+
content.styles !== null
15+
) {
16+
content.styles = {
17+
...content.styles,
18+
italic: true,
19+
textColor: 'gray',
20+
};
21+
}
1622
}
1723
});
1824
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './interlinkingLinkPDF';
2+
export * from './interlinkingLinkDocX';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* eslint-disable jsx-a11y/alt-text */
2+
3+
import { ExternalHyperlink, ImageRun, Paragraph, TextRun } from 'docx';
4+
5+
import DocSelectedIcon from '../assets/doc-selected.png';
6+
import { DocsExporterDocx } from '../types';
7+
8+
export const inlineContentMappingInterlinkingLinkDocx: DocsExporterDocx['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
9+
(inline) => {
10+
const fetchImageData = async () => {
11+
const response = await fetch(DocSelectedIcon.src);
12+
return response.arrayBuffer();
13+
};
14+
//const file = new FileReader();
15+
//file.readAsArrayBuffer(DocSelectedIcon.src);
16+
function dataURItoBlob(dataURI: string) {
17+
const byteString = atob(dataURI.split(',')[1]);
18+
const ab = new ArrayBuffer(byteString.length);
19+
const ia = new Uint8Array(ab);
20+
for (let i = 0; i < byteString.length; i++) {
21+
ia[i] = byteString.charCodeAt(i);
22+
}
23+
const bb = new Blob([ab]);
24+
return bb;
25+
}
26+
27+
const imageDataPromise = fetchImageData();
28+
29+
return new Paragraph({
30+
children: [
31+
new ExternalHyperlink({
32+
children: [
33+
new ImageRun({
34+
data: dataURItoBlob(DocSelectedIcon.src).arrayBuffer(),
35+
transformation: {
36+
width: 12,
37+
height: 12,
38+
},
39+
}),
40+
new TextRun({
41+
text: inline.props.title,
42+
style: 'Hyperlink',
43+
}),
44+
],
45+
link: window.location.origin + inline.props.url,
46+
}),
47+
],
48+
});
49+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* eslint-disable jsx-a11y/alt-text */
2+
import { Image, Link } from '@react-pdf/renderer';
3+
4+
import DocSelectedIcon from '../assets/doc-selected.png';
5+
import { DocsExporterPDF } from '../types';
6+
7+
export const inlineContentMappingInterlinkingLinkPDF: DocsExporterPDF['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
8+
(inline) => {
9+
return (
10+
<Link
11+
src={window.location.origin + inline.props.url}
12+
style={{
13+
textDecoration: 'none',
14+
color: 'black',
15+
}}
16+
>
17+
{' '}
18+
<Image src={DocSelectedIcon.src} />
19+
{inline.props.title}{' '}
20+
</Link>
21+
);
22+
};

src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter';
2+
import { Paragraph } from 'docx';
23

34
import {
45
blockMappingDividerDocx,
56
blockMappingImageDocx,
67
blockMappingQuoteDocx,
78
} from './blocks-mapping';
9+
import { inlineContentMappingInterlinkingLinkDocx } from './inline-content-mapping';
810
import { DocsExporterDocx } from './types';
911

1012
export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
@@ -15,4 +17,9 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
1517
quote: blockMappingQuoteDocx,
1618
image: blockMappingImageDocx,
1719
},
20+
inlineContentMapping: {
21+
...docxDefaultSchemaMappings.inlineContentMapping,
22+
interlinkingSearchInline: () => new Paragraph(''),
23+
interlinkingLinkInline: inlineContentMappingInterlinkingLinkDocx,
24+
},
1825
};

src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
blockMappingQuotePDF,
99
blockMappingTablePDF,
1010
} from './blocks-mapping';
11+
import { inlineContentMappingInterlinkingLinkPDF } from './inline-content-mapping';
1112
import { DocsExporterPDF } from './types';
1213

1314
export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
@@ -21,4 +22,9 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
2122
quote: blockMappingQuotePDF,
2223
table: blockMappingTablePDF,
2324
},
25+
inlineContentMapping: {
26+
...pdfDefaultSchemaMappings.inlineContentMapping,
27+
interlinkingSearchInline: () => <></>,
28+
interlinkingLinkInline: inlineContentMappingInterlinkingLinkPDF,
29+
},
2430
};

0 commit comments

Comments
 (0)