🏗️(frontend) blockMapping refactoring
As made for TablePDF, we separate the block mapping in separate files. This will allow us to have a better separation of concerns and to have a more maintainable codebase. We improve as well the typing. It will be easier to add new blocks in the future.
This commit is contained in:
@@ -17,8 +17,12 @@ export type HeadingBlock = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DocsBlockSchema = typeof blockNoteSchema.blockSchema;
|
||||||
|
export type DocsInlineContentSchema =
|
||||||
|
typeof blockNoteSchema.inlineContentSchema;
|
||||||
|
export type DocsStyleSchema = typeof blockNoteSchema.styleSchema;
|
||||||
export type DocsBlockNoteEditor = BlockNoteEditor<
|
export type DocsBlockNoteEditor = BlockNoteEditor<
|
||||||
typeof blockNoteSchema.blockSchema,
|
DocsBlockSchema,
|
||||||
typeof blockNoteSchema.inlineContentSchema,
|
DocsInlineContentSchema,
|
||||||
typeof blockNoteSchema.styleSchema
|
DocsStyleSchema
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Text } from '@react-pdf/renderer';
|
||||||
|
|
||||||
|
import { DocsExporterPDF } from '../types';
|
||||||
|
|
||||||
|
export const blockMappingHeadingPDF: DocsExporterPDF['mappings']['blockMapping']['heading'] =
|
||||||
|
(block, exporter) => {
|
||||||
|
const PIXELS_PER_POINT = 0.75;
|
||||||
|
const MERGE_RATIO = 7.5;
|
||||||
|
const FONT_SIZE = 16;
|
||||||
|
const fontSizeEM =
|
||||||
|
block.props.level === 1 ? 2 : block.props.level === 2 ? 1.5 : 1.17;
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={block.id}
|
||||||
|
style={{
|
||||||
|
fontSize: fontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
|
||||||
|
fontWeight: 700,
|
||||||
|
marginTop: `${fontSizeEM * MERGE_RATIO}px`,
|
||||||
|
marginBottom: `${fontSizeEM * MERGE_RATIO}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{exporter.transformInlineContent(block.content)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './headingPDF';
|
||||||
|
export * from './paragraphPDF';
|
||||||
|
export * from './tablePDF';
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Text } from '@react-pdf/renderer';
|
||||||
|
|
||||||
|
import { DocsExporterPDF } from '../types';
|
||||||
|
|
||||||
|
export const blockMappingParagraphPDF: DocsExporterPDF['mappings']['blockMapping']['paragraph'] =
|
||||||
|
(block, exporter) => {
|
||||||
|
/**
|
||||||
|
* Breakline in the editor are not rendered in the PDF
|
||||||
|
* By adding a space if the block is empty we ensure that the block is rendered
|
||||||
|
*/
|
||||||
|
if (Array.isArray(block.content)) {
|
||||||
|
block.content.forEach((content) => {
|
||||||
|
if (content.type === 'text' && !content.text) {
|
||||||
|
content.text = ' ';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!block.content.length) {
|
||||||
|
block.content.push({
|
||||||
|
styles: {},
|
||||||
|
text: ' ',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text key={block.id}>
|
||||||
|
{exporter.transformInlineContent(block.content)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { TD, TH, TR, Table } from '@ag-media/react-pdf-table';
|
||||||
|
import { View } from '@react-pdf/renderer';
|
||||||
|
|
||||||
|
import { DocsExporterPDF } from '../types';
|
||||||
|
|
||||||
|
export const blockMappingTablePDF: DocsExporterPDF['mappings']['blockMapping']['table'] =
|
||||||
|
(block, exporter) => {
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
{block.content.rows.map((row, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return (
|
||||||
|
<TH key={index}>
|
||||||
|
{row.cells.map((cell, index) => {
|
||||||
|
// Make empty cells are rendered.
|
||||||
|
if (cell.length === 0) {
|
||||||
|
cell.push({
|
||||||
|
styles: {},
|
||||||
|
text: ' ',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TD key={index}>{exporter.transformInlineContent(cell)}</TD>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TH>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TR key={index}>
|
||||||
|
{row.cells.map((cell, index) => {
|
||||||
|
// Make empty cells are rendered.
|
||||||
|
if (cell.length === 0) {
|
||||||
|
cell.push({
|
||||||
|
styles: {},
|
||||||
|
text: ' ',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TD key={index}>
|
||||||
|
<View>{exporter.transformInlineContent(cell)}</View>
|
||||||
|
</TD>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TR>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
import {
|
import { DOCXExporter } from '@blocknote/xl-docx-exporter';
|
||||||
DOCXExporter,
|
import { PDFExporter } from '@blocknote/xl-pdf-exporter';
|
||||||
docxDefaultSchemaMappings,
|
|
||||||
} from '@blocknote/xl-docx-exporter';
|
|
||||||
import {
|
|
||||||
PDFExporter,
|
|
||||||
pdfDefaultSchemaMappings,
|
|
||||||
} from '@blocknote/xl-pdf-exporter';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Loader,
|
Loader,
|
||||||
@@ -15,7 +9,7 @@ import {
|
|||||||
VariantType,
|
VariantType,
|
||||||
useToastProvider,
|
useToastProvider,
|
||||||
} from '@openfun/cunningham-react';
|
} from '@openfun/cunningham-react';
|
||||||
import { Text as PDFText, pdf } from '@react-pdf/renderer';
|
import { pdf } from '@react-pdf/renderer';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
@@ -25,10 +19,10 @@ import { useEditorStore } from '@/features/docs/doc-editor';
|
|||||||
import { Doc, useTrans } from '@/features/docs/doc-management';
|
import { Doc, useTrans } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
|
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
|
||||||
|
import { docxDocsSchemaMappings } from '../mappingDocx';
|
||||||
|
import { pdfDocsSchemaMappings } from '../mappingPDF';
|
||||||
import { downloadFile, exportResolveFileUrl } from '../utils';
|
import { downloadFile, exportResolveFileUrl } from '../utils';
|
||||||
|
|
||||||
import { Table } from './blocks/Table';
|
|
||||||
|
|
||||||
enum DocDownloadFormat {
|
enum DocDownloadFormat {
|
||||||
PDF = 'pdf',
|
PDF = 'pdf',
|
||||||
DOCX = 'docx',
|
DOCX = 'docx',
|
||||||
@@ -96,91 +90,25 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
|||||||
if (format === DocDownloadFormat.PDF) {
|
if (format === DocDownloadFormat.PDF) {
|
||||||
const defaultExporter = new PDFExporter(
|
const defaultExporter = new PDFExporter(
|
||||||
editor.schema,
|
editor.schema,
|
||||||
pdfDefaultSchemaMappings,
|
pdfDocsSchemaMappings,
|
||||||
);
|
);
|
||||||
|
|
||||||
const exporter = new PDFExporter(
|
const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, {
|
||||||
editor.schema,
|
resolveFileUrl: async (url) =>
|
||||||
{
|
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
|
||||||
...pdfDefaultSchemaMappings,
|
});
|
||||||
blockMapping: {
|
|
||||||
...pdfDefaultSchemaMappings.blockMapping,
|
|
||||||
heading: (block, exporter) => {
|
|
||||||
const PIXELS_PER_POINT = 0.75;
|
|
||||||
const MERGE_RATIO = 7.5;
|
|
||||||
const FONT_SIZE = 16;
|
|
||||||
const fontSizeEM =
|
|
||||||
block.props.level === 1
|
|
||||||
? 2
|
|
||||||
: block.props.level === 2
|
|
||||||
? 1.5
|
|
||||||
: 1.17;
|
|
||||||
return (
|
|
||||||
<PDFText
|
|
||||||
key={block.id}
|
|
||||||
style={{
|
|
||||||
fontSize: fontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
|
|
||||||
fontWeight: 700,
|
|
||||||
marginTop: `${fontSizeEM * MERGE_RATIO}px`,
|
|
||||||
marginBottom: `${fontSizeEM * MERGE_RATIO}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{exporter.transformInlineContent(block.content)}
|
|
||||||
</PDFText>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
paragraph: (block, exporter) => {
|
|
||||||
/**
|
|
||||||
* Breakline in the editor are not rendered in the PDF
|
|
||||||
* By adding a space if the block is empty we ensure that the block is rendered
|
|
||||||
*/
|
|
||||||
if (Array.isArray(block.content)) {
|
|
||||||
block.content.forEach((content) => {
|
|
||||||
if (content.type === 'text' && !content.text) {
|
|
||||||
content.text = ' ';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!block.content.length) {
|
|
||||||
block.content.push({
|
|
||||||
styles: {},
|
|
||||||
text: ' ',
|
|
||||||
type: 'text',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<PDFText key={block.id}>
|
|
||||||
{exporter.transformInlineContent(block.content)}
|
|
||||||
</PDFText>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
table: (block, transformer) => {
|
|
||||||
return <Table data={block.content} transformer={transformer} />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resolveFileUrl: async (url) =>
|
|
||||||
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const pdfDocument = await exporter.toReactPDFDocument(exportDocument);
|
const pdfDocument = await exporter.toReactPDFDocument(exportDocument);
|
||||||
blobExport = await pdf(pdfDocument).toBlob();
|
blobExport = await pdf(pdfDocument).toBlob();
|
||||||
} else {
|
} else {
|
||||||
const defaultExporter = new DOCXExporter(
|
const defaultExporter = new DOCXExporter(
|
||||||
editor.schema,
|
editor.schema,
|
||||||
docxDefaultSchemaMappings,
|
docxDocsSchemaMappings,
|
||||||
);
|
);
|
||||||
|
|
||||||
const exporter = new DOCXExporter(
|
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {
|
||||||
editor.schema,
|
resolveFileUrl: async (url) =>
|
||||||
docxDefaultSchemaMappings,
|
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
|
||||||
{
|
});
|
||||||
resolveFileUrl: async (url) =>
|
|
||||||
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
blobExport = await exporter.toBlob(exportDocument);
|
blobExport = await exporter.toBlob(exportDocument);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { TD, TH, TR, Table as TablePDF } from '@ag-media/react-pdf-table';
|
|
||||||
import {
|
|
||||||
DefaultBlockSchema,
|
|
||||||
Exporter,
|
|
||||||
InlineContentSchema,
|
|
||||||
StyleSchema,
|
|
||||||
TableContent,
|
|
||||||
} from '@blocknote/core';
|
|
||||||
import { View } from '@react-pdf/renderer';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export const Table = (props: {
|
|
||||||
data: TableContent<InlineContentSchema>;
|
|
||||||
transformer: Exporter<
|
|
||||||
DefaultBlockSchema,
|
|
||||||
InlineContentSchema,
|
|
||||||
StyleSchema,
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<TablePDF>
|
|
||||||
{props.data.rows.map((row, index) => {
|
|
||||||
if (index === 0) {
|
|
||||||
return (
|
|
||||||
<TH key={index}>
|
|
||||||
{row.cells.map((cell, index) => {
|
|
||||||
// Make empty cells are rendered.
|
|
||||||
if (cell.length === 0) {
|
|
||||||
cell.push({
|
|
||||||
styles: {},
|
|
||||||
text: ' ',
|
|
||||||
type: 'text',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TD key={index}>
|
|
||||||
{props.transformer.transformInlineContent(cell)}
|
|
||||||
</TD>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TH>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TR key={index}>
|
|
||||||
{row.cells.map((cell, index) => {
|
|
||||||
// Make empty cells are rendered.
|
|
||||||
if (cell.length === 0) {
|
|
||||||
cell.push({
|
|
||||||
styles: {},
|
|
||||||
text: ' ',
|
|
||||||
type: 'text',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TD key={index}>
|
|
||||||
<View>
|
|
||||||
{
|
|
||||||
props.transformer.transformInlineContent(
|
|
||||||
cell,
|
|
||||||
) as ReactNode
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</TD>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TR>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TablePDF>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter';
|
||||||
|
|
||||||
|
import { DocsExporterDocx } from './types';
|
||||||
|
|
||||||
|
export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
|
||||||
|
...docxDefaultSchemaMappings,
|
||||||
|
blockMapping: {
|
||||||
|
...docxDefaultSchemaMappings.blockMapping,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { pdfDefaultSchemaMappings } from '@blocknote/xl-pdf-exporter';
|
||||||
|
|
||||||
|
import {
|
||||||
|
blockMappingHeadingPDF,
|
||||||
|
blockMappingParagraphPDF,
|
||||||
|
blockMappingTablePDF,
|
||||||
|
} from './blocks-mapping';
|
||||||
|
import { DocsExporterPDF } from './types';
|
||||||
|
|
||||||
|
export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
|
||||||
|
...pdfDefaultSchemaMappings,
|
||||||
|
blockMapping: {
|
||||||
|
...pdfDefaultSchemaMappings.blockMapping,
|
||||||
|
heading: blockMappingHeadingPDF,
|
||||||
|
paragraph: blockMappingParagraphPDF,
|
||||||
|
table: blockMappingTablePDF,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,3 +1,18 @@
|
|||||||
|
import { Exporter } from '@blocknote/core';
|
||||||
|
import { Link, Text, TextProps } from '@react-pdf/renderer';
|
||||||
|
import {
|
||||||
|
IRunPropertiesOptions,
|
||||||
|
Paragraph,
|
||||||
|
ParagraphChild,
|
||||||
|
Table,
|
||||||
|
TextRun,
|
||||||
|
} from 'docx';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DocsBlockSchema,
|
||||||
|
DocsInlineContentSchema,
|
||||||
|
DocsStyleSchema,
|
||||||
|
} from '../doc-editor';
|
||||||
import { Access } from '../doc-management';
|
import { Access } from '../doc-management';
|
||||||
|
|
||||||
export interface Template {
|
export interface Template {
|
||||||
@@ -16,3 +31,23 @@ export interface Template {
|
|||||||
css: string;
|
css: string;
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DocsExporterPDF = Exporter<
|
||||||
|
NoInfer<DocsBlockSchema>,
|
||||||
|
NoInfer<DocsInlineContentSchema>,
|
||||||
|
NoInfer<DocsStyleSchema>,
|
||||||
|
React.ReactElement<Text>,
|
||||||
|
React.ReactElement<Link> | React.ReactElement<Text>,
|
||||||
|
TextProps['style'],
|
||||||
|
React.ReactElement<Text>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DocsExporterDocx = Exporter<
|
||||||
|
NoInfer<DocsBlockSchema>,
|
||||||
|
NoInfer<DocsInlineContentSchema>,
|
||||||
|
NoInfer<DocsStyleSchema>,
|
||||||
|
Promise<Paragraph[] | Paragraph | Table> | Paragraph[] | Paragraph | Table,
|
||||||
|
ParagraphChild,
|
||||||
|
IRunPropertiesOptions,
|
||||||
|
TextRun
|
||||||
|
>;
|
||||||
|
|||||||
Reference in New Issue
Block a user